From 5518c25ac57e0072d027dfa1a5f33929de158b6c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Nov 2026 02:19:36 +0100 Subject: [PATCH] chore(gpui): vendor the experimental native runtime --- .gitattributes | 3 + .gitignore | 6 + .oxfmtrc.json | 2 +- .oxlintrc.json | 2 +- vendor/react-native-gpui/LICENSE | 21 + vendor/react-native-gpui/UPSTREAM | 21 + vendor/react-native-gpui/modules/fs/README.md | 3 + .../modules/fs/desktop/Cargo.toml | 25 + .../modules/fs/desktop/build.rs | 3 + .../modules/fs/desktop/src/dialogs.rs | 394 + .../modules/fs/desktop/src/dirs.rs | 363 + .../modules/fs/desktop/src/errors.rs | 149 + .../modules/fs/desktop/src/files.rs | 640 ++ .../modules/fs/desktop/src/handles.rs | 697 ++ .../modules/fs/desktop/src/lib.rs | 258 + .../modules/fs/desktop/src/links.rs | 336 + .../modules/fs/desktop/src/module.rs | 187 + .../modules/fs/desktop/src/paths.rs | 231 + .../modules/fs/desktop/src/watching.rs | 392 + .../react-native-gpui/modules/fs/package.json | 19 + .../modules/fs/react-native.config.js | 9 + .../modules/fs/src/NativeFs.ts | 162 + .../modules/fs/src/constants.ts | 43 + .../react-native-gpui/modules/fs/src/index.ts | 769 ++ .../modules/fs/src/promises.ts | 771 ++ .../modules/fs/src/shared.ts | 539 ++ .../react-native-gpui/modules/fs/src/stats.ts | 196 + .../react-native-gpui/modules/fs/src/watch.ts | 141 + .../gesture-handler/desktop/CMakeLists.txt | 19 + .../desktop/cpp/GPUIGestureHandlerModule.cpp | 500 ++ .../modules/gesture-handler/package.json | 22 + .../gesture-handler/react-native.config.js | 25 + .../src/GestureButtons.desktop.tsx | 71 + .../src/NativeRNGestureHandlerModule.ts | 32 + .../src/customDirectEventTypes.desktop.ts | 13 + .../modules/reanimated/desktop/CMakeLists.txt | 94 + .../desktop/cpp/GPUIReanimatedModule.cpp | 161 + .../modules/reanimated/package.json | 22 + .../modules/reanimated/react-native.config.js | 10 + .../reanimated/src/NativeReanimatedModule.ts | 8 + .../safe-area-context/desktop/CMakeLists.txt | 33 + .../desktop/cpp/GPUISafeAreaContextModule.cpp | 48 + .../desktop/cpp/GPUISafeAreaInsets.h | 22 + .../desktop/cpp/GpuiSafeAreaModule.cpp | 143 + .../modules/safe-area-context/package.json | 20 + .../safe-area-context/react-native.config.js | 19 + .../src/NativeSafeAreaContext.ts | 23 + .../modules/screens/desktop/CMakeLists.txt | 30 + .../modules/screens/desktop/Cargo.toml | 54 + .../modules/screens/desktop/build.rs | 38 + .../screens/desktop/cpp/GpuiScreensModule.cpp | 77 + .../modules/screens/desktop/src/lib.rs | 29 + .../modules/screens/desktop/src/lifecycle.rs | 80 + .../modules/screens/desktop/src/props.rs | 177 + .../modules/screens/desktop/src/render.rs | 829 ++ .../modules/screens/desktop/src/stack.rs | 396 + .../modules/screens/desktop/src/state.rs | 162 + .../modules/screens/desktop/src/transition.rs | 193 + .../screens/desktop/src/unsupported.rs | 39 + .../modules/screens/package.json | 15 + .../modules/screens/react-native.config.js | 36 + .../screens/src/FullWindowOverlay.desktop.tsx | 19 + .../modules/screens/src/core.desktop.ts | 28 + .../src/customDirectEventTypes.desktop.ts | 16 + .../modules/svg/desktop/CMakeLists.txt | 8 + .../modules/svg/desktop/Cargo.toml | 10 + .../modules/svg/desktop/build.rs | 38 + .../modules/svg/desktop/cpp/GpuiSvgModule.cpp | 141 + .../modules/svg/desktop/src/lib.rs | 465 + .../modules/svg/package.json | 14 + .../modules/svg/react-native.config.js | 51 + .../modules/worklets/desktop/CMakeLists.txt | 35 + .../desktop/cpp/GPUIPlatformLogger.cpp | 27 + .../desktop/cpp/GPUIWorkletsModule.cpp | 336 + .../worklets/desktop/cpp/GPUIWorkletsModule.h | 60 + .../desktop/cpp/GpuiWorkletsFrameModule.cpp | 13 + .../modules/worklets/package.json | 20 + .../modules/worklets/react-native.config.js | 10 + .../worklets/src/NativeWorkletsModule.ts | 10 + .../packages/cli/assets/default-icon.png | Bin 0 -> 8867 bytes .../packages/cli/bin/react-native-gpui.js | 167 + .../packages/cli/desktop.schema.json | 249 + .../packages/cli/package.json | 18 + .../packages/cli/react-native.config.cjs | 207 + .../packages/cli/src/autolink.js | 1462 ++++ .../packages/cli/src/autolink.test.js | 697 ++ .../packages/cli/src/commands.js | 1782 ++++ .../packages/cli/src/commands.test.js | 469 + .../packages/cli/src/desktop-config.js | 397 + .../packages/cli/src/desktop-config.test.js | 308 + .../packages/cli/src/package-desktop.js | 718 ++ .../packages/cli/src/package-desktop.test.js | 687 ++ .../packages/cli/src/target-gc.js | 386 + .../packages/cli/src/target-gc.test.js | 224 + .../packages/cli/templates/app/App.tsx | 38 + .../cli/templates/app/babel.config.js | 10 + .../templates/app/desktop/.cargo/config.toml | 6 + .../cli/templates/app/desktop/README.md | 128 + .../templates/app/desktop/assets/README.md | 4 + .../cli/templates/app/desktop/assets/icon.png | Bin 0 -> 8867 bytes .../cli/templates/app/desktop/build.rs | 172 + .../cli/templates/app/desktop/desktop.json | 9 + .../cli/templates/app/desktop/e2e/app.e2e.ts | 17 + .../cli/templates/app/desktop/gitignore | 3 + .../cli/templates/app/desktop/metro.config.js | 6 + .../cli/templates/app/desktop/src/main.rs | 41 + .../app/desktop/vitest.e2e.config.ts | 14 + .../cli/templates/modules/cpp/CMakeLists.txt | 3 + .../cli/templates/modules/cpp/NativeModule.ts | 7 + .../cli/templates/modules/cpp/RNModule.cpp | 29 + .../cli/templates/modules/cpp/RNModule.hpp | 13 + .../cli/templates/modules/cpp/index.ts | 1 + .../templates/modules/rust/NativeModule.ts | 7 + .../cli/templates/modules/rust/build.rs | 3 + .../cli/templates/modules/rust/index.ts | 1 + .../cli/templates/modules/rust/lib.rs | 26 + .../packages/codegen/package.json | 21 + .../packages/codegen/src/generate-cxx-spec.js | 341 + .../codegen/src/generate-cxx-spec.test.js | 193 + .../src/generate-host-component-cpp.js | 861 ++ .../src/generate-host-component-cpp.test.js | 437 + .../src/generate-rust-spec.component.test.js | 444 + .../codegen/src/generate-rust-spec.js | 2906 ++++++ .../codegen/src/generate-rust-spec.test.js | 273 + .../packages/codegen/src/kitchen-sink-spec.js | 56 + .../packages/core/NativeViewHost.tsx | 23 + .../core/NativeViewHostNativeComponent.ts | 10 + .../react/devsupport/DevServerHelper.cpp | 235 + .../react/nativemodule/dom/NativeDOM.cpp | 485 + .../react/nativemodule/dom/NativeDOM.h | 116 + .../textinput/BaseTextInputShadowNode.h | 239 + .../components/view/HostPlatformViewProps.h | 71 + .../view/HostPlatformViewTraitsInitializer.h | 22 + .../textlayoutmanager/TextLayoutManager.cpp | 71 + .../react/coremodules/AppStateModule.cpp | 28 + .../react/coremodules/AppStateModule.h | 38 + .../react/coremodules/CMakeLists.txt | 23 + .../react/coremodules/DeviceInfoModule.cpp | 49 + .../react/coremodules/DeviceInfoModule.h | 51 + .../coremodules/PlatformConstantsModule.cpp | 47 + .../coremodules/PlatformConstantsModule.h | 60 + .../react/devsupport/CMakeLists.txt | 35 + .../react/devsupport/DevLoadingViewModule.cpp | 47 + .../react/devsupport/DevLoadingViewModule.h | 36 + .../react/devsupport/DevServerHelper.cpp | 164 + .../react/devsupport/DevServerHelper.h | 82 + .../react/devsupport/DevSettingsModule.cpp | 87 + .../react/devsupport/DevSettingsModule.h | 60 + .../react/devsupport/IDevUIDelegate.h | 32 + .../react/devsupport/LogBoxModule.cpp | 42 + .../react/devsupport/LogBoxModule.h | 26 + .../react/devsupport/PackagerConnection.cpp | 89 + .../react/devsupport/PackagerConnection.h | 49 + .../react/devsupport/SourceCodeModule.cpp | 16 + .../react/devsupport/SourceCodeModule.h | 34 + .../react/devsupport/inspector/Inspector.cpp | 232 + .../react/devsupport/inspector/Inspector.h | 68 + .../InspectorPackagerConnectionDelegate.cpp | 109 + .../InspectorPackagerConnectionDelegate.h | 62 + .../devsupport/inspector/InspectorThread.h | 29 + .../react/http/CMakeLists.txt | 20 + .../react/http/IHttpClient.cpp | 16 + .../ReactCxxPlatform/react/http/IHttpClient.h | 98 + .../react/http/IWebSocketClient.cpp | 17 + .../react/http/IWebSocketClient.h | 70 + .../http/platform/cxx/WebSocketClient.cpp | 610 ++ .../react/http/platform/cxx/WebSocketClient.h | 50 + .../ReactCxxPlatform/react/io/BlobModule.cpp | 133 + .../ReactCxxPlatform/react/io/BlobModule.h | 33 + .../ReactCxxPlatform/react/io/CMakeLists.txt | 26 + .../react/io/FileReaderModule.cpp | 58 + .../react/io/FileReaderModule.h | 27 + .../ReactCxxPlatform/react/io/IImageLoader.h | 32 + .../react/io/ImageLoaderModule.cpp | 120 + .../react/io/ImageLoaderModule.h | 56 + .../react/io/NetworkingModule.cpp | 404 + .../react/io/NetworkingModule.h | 127 + .../react/io/ResourceLoader.cpp | 70 + .../react/io/ResourceLoader.h | 34 + .../react/io/WebSocketModule.cpp | 252 + .../react/io/WebSocketModule.h | 55 + .../platform/android/AssetManagerHelpers.cpp | 60 + .../io/platform/android/AssetManagerHelpers.h | 21 + .../io/platform/android/ResourceLoader.cpp | 66 + .../react/io/platform/cxx/ResourceLoader.cpp | 31 + .../react/io/tests/NetworkingModuleTests.cpp | 115 + .../react/io/tests/TestJsRuntime.h | 39 + .../ReactCxxPlatform/react/jni/JniHelper.cpp | 30 + .../ReactCxxPlatform/react/jni/JniHelper.h | 19 + .../react/logging/CMakeLists.txt | 24 + .../react/logging/DefaultLogger.cpp | 35 + .../react/logging/DefaultLogger.h | 19 + .../react/logging/DefaultOnJsErrorHandler.h | 22 + .../react/logging/LogOnce.cpp | 43 + .../ReactCxxPlatform/react/logging/LogOnce.h | 33 + .../react/logging/NativeExceptionsManager.cpp | 70 + .../react/logging/NativeExceptionsManager.h | 74 + .../react/nativemodule/CMakeLists.txt | 20 + .../react/nativemodule/JavaScriptModule.h | 19 + .../react/nativemodule/TurboModuleProvider.h | 25 + .../react/profiling/CMakeLists.txt | 22 + .../react/profiling/FrameStats.cpp | 72 + .../react/profiling/FrameStats.h | 49 + .../profiling/RuntimeHeapInfoCollector.cpp | 53 + .../profiling/RuntimeHeapInfoCollector.h | 48 + .../react/profiling/TimeSeries.cpp | 330 + .../react/profiling/TimeSeries.h | 131 + .../react/profiling/Tracing.cpp | 12 + .../react/profiling/perfetto.cpp | 30 + .../react/profiling/perfetto.h | 39 + .../react/profiling/tests/TimeSeriesTests.cpp | 206 + .../react/profiling/tracy_noop.h | 42 + .../DesktopContextMenuView.cpp | 83 + .../DesktopContextMenuView.h | 61 + .../nativeviewhost/NativeViewHost.cpp | 60 + .../nativeviewhost/NativeViewHost.h | 41 + .../shortcutscope/ShortcutScope.cpp | 56 + .../components/shortcutscope/ShortcutScope.h | 56 + .../windowcontrol/WindowControl.cpp | 75 + .../components/windowcontrol/WindowControl.h | 51 + .../react/renderer/scheduler/CMakeLists.txt | 22 + .../scheduler/SchedulerDelegateImpl.cpp | 96 + .../scheduler/SchedulerDelegateImpl.h | 64 + .../renderer/scheduler/SurfaceDelegate.h | 46 + .../react/renderer/uimanager/CMakeLists.txt | 21 + .../renderer/uimanager/IMountingManager.h | 92 + .../react/runtime/CMakeLists.txt | 51 + .../react/runtime/GpuiDebug.h | 12 + .../runtime/PlatformTimerRegistryImpl.cpp | 142 + .../react/runtime/PlatformTimerRegistryImpl.h | 63 + .../runtime/ReactCxxTurboModuleProvider.cpp | 127 + .../runtime/ReactCxxTurboModuleProvider.h | 58 + .../react/runtime/ReactHost.cpp | 557 ++ .../react/runtime/ReactHost.h | 150 + .../react/runtime/ReactInstanceConfig.h | 52 + .../react/threading/CMakeLists.txt | 21 + .../threading/MessageQueueThreadImpl.cpp | 53 + .../react/threading/MessageQueueThreadImpl.h | 51 + .../react/threading/TaskDispatchThread.cpp | 154 + .../react/threading/TaskDispatchThread.h | 74 + .../react/threading/tests/.clang-tidy | 5 + .../tests/TaskDispatchThreadTests.cpp | 207 + .../react/utils/CMakeLists.txt | 22 + .../react/utils/PlatformRunLoopObserver.h | 48 + .../react/utils/RunLoopObserverManager.cpp | 59 + .../react/utils/RunLoopObserverManager.h | 32 + .../packages/core/ReactGpui/Cargo.lock | 7775 +++++++++++++++++ .../packages/core/ReactGpui/Cargo.toml | 57 + .../packages/core/ReactGpui/GPUI_PIN.md | 202 + .../ReactGpui/rn-gpui-module-build/Cargo.toml | 8 + .../ReactGpui/rn-gpui-module-build/src/lib.rs | 165 + .../core/ReactGpui/rn-gpui-module/Cargo.toml | 7 + .../ReactGpui/rn-gpui-module/src/error.rs | 45 + .../core/ReactGpui/rn-gpui-module/src/host.rs | 208 + .../core/ReactGpui/rn-gpui-module/src/lib.rs | 44 + .../ReactGpui/rn-gpui-module/src/private.rs | 119 + .../ReactGpui/rn-gpui-module/src/queue.rs | 105 + .../ReactGpui/rn-gpui-module/src/registry.rs | 104 + .../ReactGpui/rn-gpui-module/src/value.rs | 297 + .../rn-gpui-module/tests/generated/Cargo.toml | 15 + .../rn-gpui-module/tests/generated/build.rs | 3 + .../tests/generated/specs/NativeProbe.ts | 62 + .../rn-gpui-module/tests/generated/src/lib.rs | 509 ++ .../ReactGpui/rn-gpui-module/tests/module.rs | 419 + .../core/ReactGpui/rn-gpui-sys/Cargo.toml | 15 + .../core/ReactGpui/rn-gpui-sys/build.rs | 1131 +++ .../ReactGpui/rn-gpui-sys/cpp/CMakeLists.txt | 1181 +++ .../rn-gpui-sys/cpp/GpuiAccessibilityMac.mm | 167 + .../ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.cpp | 196 + .../ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.h | 54 + .../ReactGpui/rn-gpui-sys/cpp/GpuiHost.cpp | 1081 +++ .../core/ReactGpui/rn-gpui-sys/cpp/GpuiHost.h | 164 + .../rn-gpui-sys/cpp/GpuiHostComponent.cpp | 189 + .../rn-gpui-sys/cpp/GpuiHostComponent.h | 42 + .../rn-gpui-sys/cpp/GpuiHostComponentsCpp.h | 288 + .../rn-gpui-sys/cpp/GpuiHttpBridge.cpp | 193 + .../rn-gpui-sys/cpp/GpuiHttpBridge.h | 28 + .../rn-gpui-sys/cpp/GpuiHttpClient.cpp | 563 ++ .../rn-gpui-sys/cpp/GpuiHttpClient.h | 15 + .../rn-gpui-sys/cpp/GpuiImageLoader.h | 12 + .../rn-gpui-sys/cpp/GpuiJsEngine.cpp | 125 + .../ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.h | 94 + .../cpp/GpuiLogBoxSurfaceDelegate.cpp | 49 + .../cpp/GpuiLogBoxSurfaceDelegate.h | 31 + .../ReactGpui/rn-gpui-sys/cpp/GpuiModules.cpp | 80 + .../ReactGpui/rn-gpui-sys/cpp/GpuiModules.h | 160 + .../rn-gpui-sys/cpp/GpuiMountingManager.cpp | 609 ++ .../rn-gpui-sys/cpp/GpuiMountingManager.h | 127 + .../rn-gpui-sys/cpp/GpuiNodeAddons.cpp | 442 + .../rn-gpui-sys/cpp/GpuiNodeAddons.h | 31 + .../rn-gpui-sys/cpp/GpuiRustTurboModules.cpp | 662 ++ .../rn-gpui-sys/cpp/GpuiRustTurboModules.h | 47 + .../rn-gpui-sys/cpp/GpuiStartupSignpost.cpp | 47 + .../rn-gpui-sys/cpp/GpuiStartupSignpost.h | 18 + .../rn-gpui-sys/cpp/GpuiTextMeasure.h | 30 + .../rn-gpui-sys/cpp/GpuiTurboModule.cpp | 171 + .../rn-gpui-sys/cpp/GpuiTurboModule.h | 635 ++ .../rn-gpui-sys/cpp/MountBatchBuilder.cpp | 441 + .../rn-gpui-sys/cpp/MountBatchBuilder.h | 24 + .../rn-gpui-sys/cpp/android-shim/README.md | 7 + .../jni-lib-merge/SoMerging-utils.cmake | 27 + .../GpuiContextMenuViewProps.cpp | 42 + .../cpp/builtins/image/GpuiImageProps.cpp | 47 + .../cpp/builtins/modal/GpuiModalProps.cpp | 29 + .../scrollview/GpuiScrollViewProps.cpp | 51 + .../shortcut_scope/GpuiShortcutScopeProps.cpp | 41 + .../builtins/textinput/GpuiTextInputProps.cpp | 68 + .../window_control/GpuiWindowControlProps.cpp | 47 + .../cpp/cmake/DownloadNativeDeps.cmake | 333 + .../rn-gpui-sys/cpp/cmake/NodeModules.cmake | 28 + .../cpp/cmake/glog-windows-config.h | 33 + .../cpp/cmake/modules/PrebuiltInstall.cmake | 400 + .../cpp/cmake/modules/ThirdParty.cmake | 84 + .../rn-gpui-sys/cpp/cmake/msvc-preinclude.h | 59 + .../rn-gpui-sys/cpp/jni/CMakeLists.txt | 32 + .../rn-gpui-sys/cpp/rn_gpui_linux_prefix.h | 17 + .../cpp/test/gpui_js_engine_test.cpp | 143 + .../cpp/test/gpui_turbo_module_test.cpp | 117 + .../include/fast_float/ascii_number.h | 903 ++ .../fast_float/include/fast_float/bigint.h | 638 ++ .../fast_float/constexpr_feature_detect.h | 53 + .../include/fast_float/decimal_to_binary.h | 238 + .../include/fast_float/digit_comparison.h | 450 + .../include/fast_float/fast_float.h | 91 + .../include/fast_float/fast_table.h | 708 ++ .../include/fast_float/float_common.h | 1419 +++ .../include/fast_float/parse_number.h | 627 ++ .../scripts/generate-fbreactnativespec.cjs | 45 + .../core/ReactGpui/rn-gpui-sys/src/lib.rs | 761 ++ .../ReactGpui/rn-gpui-sys/src/rust_module.rs | 481 + .../core/ReactGpui/rn-gpui/Cargo.toml | 99 + .../packages/core/ReactGpui/rn-gpui/build.rs | 69 + .../ReactGpui/rn-gpui/src/accessibility.rs | 159 + .../ReactGpui/rn-gpui/src/animation_policy.rs | 41 + .../core/ReactGpui/rn-gpui/src/bootstrap.rs | 2345 +++++ .../rn-gpui/src/builtins/alert/mod.rs | 89 + .../rn-gpui/src/builtins/app_menu/mod.rs | 146 + .../rn-gpui/src/builtins/clipboard/mod.rs | 41 + .../rn-gpui/src/builtins/context_menu/mod.rs | 113 + .../src/builtins/context_menu_view/mod.rs | 285 + .../rn-gpui/src/builtins/dev_menu/mod.rs | 53 + .../rn-gpui/src/builtins/image/mod.rs | 52 + .../rn-gpui/src/builtins/linking/mod.rs | 52 + .../rn-gpui/src/builtins/main_thread.rs | 15 + .../ReactGpui/rn-gpui/src/builtins/mod.rs | 39 + .../rn-gpui/src/builtins/modal/mod.rs | 33 + .../rn-gpui/src/builtins/perf_monitor/mod.rs | 63 + .../rn-gpui/src/builtins/scrollview/mod.rs | 46 + .../src/builtins/shortcut_scope/mod.rs | 115 + .../rn-gpui/src/builtins/shortcuts/mod.rs | 124 + .../rn-gpui/src/builtins/textinput/input.rs | 845 ++ .../rn-gpui/src/builtins/textinput/mod.rs | 5 + .../rn-gpui/src/builtins/textinput/render.rs | 245 + .../rn-gpui/src/builtins/tray/mod.rs | 193 + .../src/builtins/window_control/mod.rs | 202 + .../rn-gpui/src/builtins/windows/mod.rs | 306 + .../core/ReactGpui/rn-gpui/src/counters.rs | 78 + .../ReactGpui/rn-gpui/src/desktop/app_menu.rs | 1500 ++++ .../rn-gpui/src/desktop/capabilities.rs | 213 + .../rn-gpui/src/desktop/client_frame.rs | 160 + .../rn-gpui/src/desktop/context_menu.rs | 515 ++ .../ReactGpui/rn-gpui/src/desktop/dev_menu.rs | 610 ++ .../ReactGpui/rn-gpui/src/desktop/errors.rs | 59 + .../rn-gpui/src/desktop/gpui_menu.rs | 969 ++ .../ReactGpui/rn-gpui/src/desktop/icon.rs | 257 + .../ReactGpui/rn-gpui/src/desktop/keymap.rs | 1151 +++ .../rn-gpui/src/desktop/menu_events.rs | 236 + .../rn-gpui/src/desktop/menu_model.rs | 592 ++ .../core/ReactGpui/rn-gpui/src/desktop/mod.rs | 71 + .../ReactGpui/rn-gpui/src/desktop/module.rs | 63 + .../rn-gpui/src/desktop/perf_monitor.rs | 413 + .../rn-gpui/src/desktop/platform_window.rs | 1068 +++ .../ReactGpui/rn-gpui/src/desktop/tray.rs | 1106 +++ .../ReactGpui/rn-gpui/src/desktop/windows.rs | 1042 +++ .../core/ReactGpui/rn-gpui/src/events.rs | 885 ++ .../ReactGpui/rn-gpui/src/extensions/input.rs | 633 ++ .../ReactGpui/rn-gpui/src/extensions/mod.rs | 1199 +++ .../core/ReactGpui/rn-gpui/src/frame.rs | 212 + .../core/ReactGpui/rn-gpui/src/host.rs | 306 + .../ReactGpui/rn-gpui/src/host_component.rs | 1046 +++ .../core/ReactGpui/rn-gpui/src/http_client.rs | 144 + .../ReactGpui/rn-gpui/src/image_loader.rs | 338 + .../core/ReactGpui/rn-gpui/src/lib.rs | 39 + .../core/ReactGpui/rn-gpui/src/main.rs | 3 + .../ReactGpui/rn-gpui/src/native_surface.rs | 457 + .../core/ReactGpui/rn-gpui/src/native_view.rs | 169 + .../rn-gpui/src/native_view_probe.rs | 305 + .../core/ReactGpui/rn-gpui/src/node_view.rs | 4658 ++++++++++ .../ReactGpui/rn-gpui/src/pointer_area.rs | 306 + .../core/ReactGpui/rn-gpui/src/resources.rs | 278 + .../core/ReactGpui/rn-gpui/src/runtime.rs | 94 + .../ReactGpui/rn-gpui/src/scroll_indicator.rs | 122 + .../ReactGpui/rn-gpui/src/scroll_layers.rs | 311 + .../core/ReactGpui/rn-gpui/src/store.rs | 2542 ++++++ .../core/ReactGpui/rn-gpui/src/style.rs | 97 + .../core/ReactGpui/rn-gpui/src/surface.rs | 653 ++ .../core/ReactGpui/rn-gpui/src/test_driver.rs | 1905 ++++ .../core/ReactGpui/rn-gpui/src/text.rs | 656 ++ .../packages/core/babel-plugin-node-addons.js | 1223 +++ .../core/babel-plugin-node-addons.test.js | 1091 +++ .../packages/core/contextMenu.test.js | 1012 +++ .../packages/core/desktop.ts | 137 + .../packages/core/index.desktop.js | 14 + .../packages/core/js-engine.js | 52 + .../packages/core/metro-transform-worker.js | 32 + .../core/metro-transform-worker.test.js | 29 + .../packages/core/metro.config.js | 271 + .../packages/core/metro.config.test.js | 145 + .../packages/core/navigation.ts | 1 + .../packages/core/overrides.json | 186 + .../packages/core/package.json | 74 + .../packages/core/polyfills/menu.tsx | 132 + .../packages/core/prebuilt-binaries.json | 39 + .../packages/core/react-native.config.js | 12 + .../packages/core/scripts/build-prebuilt.mjs | 525 ++ .../core/scripts/check-prebuilt-pin.mjs | 57 + .../core/scripts/check-standalone-cmake.mjs | 60 + .../core/scripts/generate-prebuilt-inputs.mjs | 126 + .../core/scripts/prebuilt-binaries.mjs | 494 ++ .../packages/core/scripts/prebuilt-inputs.mjs | 175 + .../core/scripts/prebuilt-inputs.test.mjs | 109 + .../packages/core/scripts/repo-root.mjs | 50 + .../packages/core/setup-env.js | 37 + .../packages/core/shortcuts.test.js | 376 + .../src-gpui/Libraries/Alert/Alert.desktop.js | 237 + .../Libraries/Components/Button.desktop.js | 401 + .../Components/StatusBar/StatusBar.desktop.js | 15 + .../Components/TextInput/TextInput.desktop.js | 134 + .../LogBox/UI/LogBoxButton.desktop.js | 99 + .../UI/LogBoxInspectorCodeFrame.desktop.js | 182 + .../UI/LogBoxInspectorHeader.desktop.js | 119 + .../LogBox/UI/LogBoxNotification.desktop.js | 93 + .../src-gpui/Libraries/Modal/Modal.desktop.js | 446 + .../NativeComponent/BaseViewConfig.desktop.ts | 37 + .../Network/RCTNetworking.desktop.js | 92 + .../ReactNativeViewConfigRegistry.desktop.js | 138 + .../Libraries/Utilities/Appearance.desktop.js | 138 + .../Utilities/BackHandler.desktop.js | 45 + .../Libraries/Utilities/Platform.desktop.js | 127 + .../Utilities/Platform.desktop.test.js | 129 + .../navigation/useDesktopForwardNavigation.ts | 105 + .../src-gpui/src/private/desktop/TitleBar.tsx | 307 + .../src/private/desktop/WindowControl.tsx | 126 + .../src/private/desktop/contextMenu.tsx | 282 + .../desktop/contextMenu/ContextMenuHost.tsx | 705 ++ .../private/desktop/contextMenu/MenuRow.tsx | 214 + .../desktop/contextMenu/MenuSurface.tsx | 162 + .../desktop/contextMenu/accelerator.ts | 157 + .../src/private/desktop/contextMenu/config.ts | 116 + .../src/private/desktop/contextMenu/levels.ts | 132 + .../private/desktop/contextMenu/navigation.ts | 269 + .../private/desktop/contextMenu/placement.ts | 160 + .../private/desktop/contextMenu/registry.ts | 90 + .../desktop/contextMenu/safeTriangle.ts | 64 + .../src/private/desktop/contextMenu/theme.ts | 121 + .../src/private/desktop/contextMenu/types.ts | 117 + .../src-gpui/src/private/desktop/index.ts | 140 + .../core/src-gpui/src/private/desktop/menu.ts | 358 + .../src/private/desktop/perfMonitor.ts | 99 + .../src-gpui/src/private/desktop/shortcuts.ts | 523 ++ .../core/src-gpui/src/private/desktop/tray.ts | 148 + .../src/private/desktop/useShortcut.tsx | 219 + .../src/private/desktop/windowContext.ts | 61 + .../src-gpui/src/private/desktop/windows.ts | 248 + .../DesktopContextMenuViewNativeComponent.ts | 63 + .../DesktopShortcutScopeNativeComponent.ts | 42 + .../DesktopWindowControlNativeComponent.ts | 37 + .../private/specs/NativeGpuiAlertManager.ts | 24 + .../src/private/specs/NativeGpuiAppMenu.ts | 36 + .../src/private/specs/NativeGpuiClipboard.ts | 10 + .../private/specs/NativeGpuiContextMenu.ts | 24 + .../src/private/specs/NativeGpuiDesktop.ts | 59 + .../src/private/specs/NativeGpuiDevMenu.ts | 15 + .../private/specs/NativeGpuiLinkingManager.ts | 13 + .../private/specs/NativeGpuiPerfMonitor.ts | 28 + .../src/private/specs/NativeGpuiShortcuts.ts | 39 + .../src/private/specs/NativeGpuiTray.ts | 61 + .../src/private/specs/NativeGpuiWindows.ts | 108 + .../packages/core/tsconfig.json | 17 + .../packages/core/types.d.ts | 81 + .../hermes/API/hermes/AsyncDebuggerAPI.h | 276 + .../packages/hermes/API/hermes/CompileJS.h | 97 + .../packages/hermes/API/hermes/DebuggerAPI.h | 502 ++ .../hermes/API/hermes/RuntimeTaskRunner.h | 64 + .../packages/hermes/API/hermes/SynthTrace.h | 1735 ++++ .../hermes/API/hermes/SynthTraceParser.h | 37 + .../hermes/API/hermes/ThreadSafetyAnalysis.h | 75 + .../packages/hermes/API/hermes/TimerStats.h | 23 + .../hermes/API/hermes/TraceInterpreter.h | 334 + .../hermes/API/hermes/TracingRuntime.h | 353 + .../packages/hermes/API/hermes/cdp/CDPAgent.h | 131 + .../hermes/API/hermes/cdp/CDPDebugAPI.h | 73 + .../hermes/API/hermes/cdp/CallbackOStream.h | 89 + .../hermes/API/hermes/cdp/ConsoleMessage.h | 135 + .../API/hermes/cdp/DebuggerDomainAgent.h | 286 + .../hermes/cdp/DebuggerDomainCoordinator.h | 137 + .../hermes/API/hermes/cdp/DomainAgent.h | 107 + .../hermes/API/hermes/cdp/DomainState.h | 140 + .../API/hermes/cdp/HeapProfilerDomainAgent.h | 71 + .../API/hermes/cdp/JSONValueInterfaces.h | 40 + .../hermes/API/hermes/cdp/MessageConverters.h | 82 + .../hermes/API/hermes/cdp/MessageInterfaces.h | 72 + .../hermes/API/hermes/cdp/MessageTypes.h | 1321 +++ .../API/hermes/cdp/MessageTypesInlines.h | 313 + .../API/hermes/cdp/ProfilerDomainAgent.h | 38 + .../API/hermes/cdp/RemoteObjectConverters.h | 77 + .../API/hermes/cdp/RemoteObjectsTable.h | 127 + .../API/hermes/cdp/RuntimeDomainAgent.h | 138 + .../hermes/API/hermes/extensions/Dummy.h | 24 + .../hermes/API/hermes/extensions/Extensions.h | 20 + .../hermes/extensions/ExtensionsBytecode.h | 23 + .../hermes/API/hermes/extensions/Intrinsics.h | 44 + .../hermes/API/hermes/extensions/JSIUtils.h | 89 + .../API/hermes/extensions/TextEncoder.h | 29 + .../hermes/API/hermes/extensions/Worker.h | 28 + .../hermes/extensions/contrib/ContribDummy.h | 24 + .../extensions/contrib/ContribExtensions.h | 23 + .../hermes/extensions/contrib/TextDecoder.h | 30 + .../extensions/contrib/TextDecoderUtils.h | 113 + .../packages/hermes/API/hermes/hermes.h | 242 + .../hermes/API/hermes/hermes_tracing.h | 52 + .../hermes/API/hermes_abi/HermesABIHelpers.h | 295 + .../API/hermes_abi/HermesABIRuntimeWrapper.h | 25 + .../hermes/API/hermes_abi/hermes_abi.h | 642 ++ .../hermes/API/hermes_abi/hermes_vtable.h | 29 + .../packages/hermes/API/jsi/jsi/JSIDynamic.h | 26 + .../packages/hermes/API/jsi/jsi/decorator.h | 1157 +++ .../hermes/API/jsi/jsi/hermes-interfaces.h | 242 + .../hermes/API/jsi/jsi/instrumentation.h | 132 + .../packages/hermes/API/jsi/jsi/jsi-inl.h | 434 + .../packages/hermes/API/jsi/jsi/jsi.h | 2266 +++++ .../packages/hermes/API/jsi/jsi/jsilib.h | 59 + .../hermes/API/jsi/jsi/test/testlib.h | 49 + .../packages/hermes/API/jsi/jsi/threadsafe.h | 79 + .../packages/hermes/API/napi/hermes_napi.h | 269 + .../hermes/API/napi/hermes_napi_compile.h | 61 + .../hermes/API/napi/hermes_napi_impl.h | 606 ++ .../hermes/API/napi/hermes_napi_internal.h | 151 + .../react-native-gpui/packages/hermes/LICENSE | 21 + .../packages/hermes/README.md | 170 + .../packages/hermes/desktop/CMakeLists.txt | 234 + .../hermes/desktop/GpuiHermesEngine.cpp | 45 + .../hermes/desktop/HermesNodeAddons.cpp | 192 + .../hermes/desktop/HermesNodeAddons.h | 22 + .../hermes/desktop/HermesNodeApiHost.cpp | 363 + .../hermes/desktop/HermesNodeApiHost.h | 100 + .../packages/hermes/desktop/bundle.js | 58 + .../hermes/desktop/node-api-exports.test.js | 105 + .../hermes/desktop/node-api-exports.txt | 155 + .../packages/hermes/desktop/runtime-files.js | 28 + .../test/hermes_node_api_addon_test.cpp | 71 + .../desktop/test/hermes_runtime_test.cpp | 31 + .../desktop/test/node_api_smoke_addon.c | 224 + .../hermes/include/hermes/napi/VENDORED.md | 22 + .../include/hermes/napi/js_native_api.h | 618 ++ .../include/hermes/napi/js_native_api_types.h | 227 + .../hermes/include/hermes/napi/node_api.h | 265 + .../include/hermes/napi/node_api_types.h | 56 + .../packages/hermes/package.json | 39 + .../0001-linux-load-icu-at-runtime.patch | 423 + .../0002-windows-load-node-api-modules.patch | 215 + .../packages/hermes/patches/README.md | 72 + .../public/hermes/Public/CrashManager.h | 107 + .../hermes/public/hermes/Public/CtorConfig.h | 145 + .../public/hermes/Public/DebuggerTypes.h | 193 + .../hermes/public/hermes/Public/GCConfig.h | 211 + .../public/hermes/Public/GCTripwireContext.h | 40 + .../public/hermes/Public/HermesExport.h | 14 + .../public/hermes/Public/JSOutOfMemoryError.h | 27 + .../public/hermes/Public/RuntimeConfig.h | 141 + .../public/hermes/Public/SamplingProfiler.h | 270 + .../packages/hermes/react-native.config.js | 23 + .../hermes/scripts/fetch-binaries.mjs | 475 + .../hermes/scripts/fetch-binaries.test.mjs | 246 + vitest.config.ts | 2 +- 575 files changed, 129495 insertions(+), 3 deletions(-) create mode 100644 .gitattributes create mode 100644 vendor/react-native-gpui/LICENSE create mode 100644 vendor/react-native-gpui/UPSTREAM create mode 100644 vendor/react-native-gpui/modules/fs/README.md create mode 100644 vendor/react-native-gpui/modules/fs/desktop/Cargo.toml create mode 100644 vendor/react-native-gpui/modules/fs/desktop/build.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/dialogs.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/dirs.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/errors.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/files.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/handles.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/lib.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/links.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/module.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/paths.rs create mode 100644 vendor/react-native-gpui/modules/fs/desktop/src/watching.rs create mode 100644 vendor/react-native-gpui/modules/fs/package.json create mode 100644 vendor/react-native-gpui/modules/fs/react-native.config.js create mode 100644 vendor/react-native-gpui/modules/fs/src/NativeFs.ts create mode 100644 vendor/react-native-gpui/modules/fs/src/constants.ts create mode 100644 vendor/react-native-gpui/modules/fs/src/index.ts create mode 100644 vendor/react-native-gpui/modules/fs/src/promises.ts create mode 100644 vendor/react-native-gpui/modules/fs/src/shared.ts create mode 100644 vendor/react-native-gpui/modules/fs/src/stats.ts create mode 100644 vendor/react-native-gpui/modules/fs/src/watch.ts create mode 100644 vendor/react-native-gpui/modules/gesture-handler/desktop/CMakeLists.txt create mode 100644 vendor/react-native-gpui/modules/gesture-handler/desktop/cpp/GPUIGestureHandlerModule.cpp create mode 100644 vendor/react-native-gpui/modules/gesture-handler/package.json create mode 100644 vendor/react-native-gpui/modules/gesture-handler/react-native.config.js create mode 100644 vendor/react-native-gpui/modules/gesture-handler/src/GestureButtons.desktop.tsx create mode 100644 vendor/react-native-gpui/modules/gesture-handler/src/NativeRNGestureHandlerModule.ts create mode 100644 vendor/react-native-gpui/modules/gesture-handler/src/customDirectEventTypes.desktop.ts create mode 100644 vendor/react-native-gpui/modules/reanimated/desktop/CMakeLists.txt create mode 100644 vendor/react-native-gpui/modules/reanimated/desktop/cpp/GPUIReanimatedModule.cpp create mode 100644 vendor/react-native-gpui/modules/reanimated/package.json create mode 100644 vendor/react-native-gpui/modules/reanimated/react-native.config.js create mode 100644 vendor/react-native-gpui/modules/reanimated/src/NativeReanimatedModule.ts create mode 100644 vendor/react-native-gpui/modules/safe-area-context/desktop/CMakeLists.txt create mode 100644 vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GPUISafeAreaContextModule.cpp create mode 100644 vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GPUISafeAreaInsets.h create mode 100644 vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GpuiSafeAreaModule.cpp create mode 100644 vendor/react-native-gpui/modules/safe-area-context/package.json create mode 100644 vendor/react-native-gpui/modules/safe-area-context/react-native.config.js create mode 100644 vendor/react-native-gpui/modules/safe-area-context/src/NativeSafeAreaContext.ts create mode 100644 vendor/react-native-gpui/modules/screens/desktop/CMakeLists.txt create mode 100644 vendor/react-native-gpui/modules/screens/desktop/Cargo.toml create mode 100644 vendor/react-native-gpui/modules/screens/desktop/build.rs create mode 100644 vendor/react-native-gpui/modules/screens/desktop/cpp/GpuiScreensModule.cpp create mode 100644 vendor/react-native-gpui/modules/screens/desktop/src/lib.rs create mode 100644 vendor/react-native-gpui/modules/screens/desktop/src/lifecycle.rs create mode 100644 vendor/react-native-gpui/modules/screens/desktop/src/props.rs create mode 100644 vendor/react-native-gpui/modules/screens/desktop/src/render.rs create mode 100644 vendor/react-native-gpui/modules/screens/desktop/src/stack.rs create mode 100644 vendor/react-native-gpui/modules/screens/desktop/src/state.rs create mode 100644 vendor/react-native-gpui/modules/screens/desktop/src/transition.rs create mode 100644 vendor/react-native-gpui/modules/screens/desktop/src/unsupported.rs create mode 100644 vendor/react-native-gpui/modules/screens/package.json create mode 100644 vendor/react-native-gpui/modules/screens/react-native.config.js create mode 100644 vendor/react-native-gpui/modules/screens/src/FullWindowOverlay.desktop.tsx create mode 100644 vendor/react-native-gpui/modules/screens/src/core.desktop.ts create mode 100644 vendor/react-native-gpui/modules/screens/src/customDirectEventTypes.desktop.ts create mode 100644 vendor/react-native-gpui/modules/svg/desktop/CMakeLists.txt create mode 100644 vendor/react-native-gpui/modules/svg/desktop/Cargo.toml create mode 100644 vendor/react-native-gpui/modules/svg/desktop/build.rs create mode 100644 vendor/react-native-gpui/modules/svg/desktop/cpp/GpuiSvgModule.cpp create mode 100644 vendor/react-native-gpui/modules/svg/desktop/src/lib.rs create mode 100644 vendor/react-native-gpui/modules/svg/package.json create mode 100644 vendor/react-native-gpui/modules/svg/react-native.config.js create mode 100644 vendor/react-native-gpui/modules/worklets/desktop/CMakeLists.txt create mode 100644 vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIPlatformLogger.cpp create mode 100644 vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIWorkletsModule.cpp create mode 100644 vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIWorkletsModule.h create mode 100644 vendor/react-native-gpui/modules/worklets/desktop/cpp/GpuiWorkletsFrameModule.cpp create mode 100644 vendor/react-native-gpui/modules/worklets/package.json create mode 100644 vendor/react-native-gpui/modules/worklets/react-native.config.js create mode 100644 vendor/react-native-gpui/modules/worklets/src/NativeWorkletsModule.ts create mode 100644 vendor/react-native-gpui/packages/cli/assets/default-icon.png create mode 100755 vendor/react-native-gpui/packages/cli/bin/react-native-gpui.js create mode 100644 vendor/react-native-gpui/packages/cli/desktop.schema.json create mode 100644 vendor/react-native-gpui/packages/cli/package.json create mode 100644 vendor/react-native-gpui/packages/cli/react-native.config.cjs create mode 100644 vendor/react-native-gpui/packages/cli/src/autolink.js create mode 100644 vendor/react-native-gpui/packages/cli/src/autolink.test.js create mode 100644 vendor/react-native-gpui/packages/cli/src/commands.js create mode 100644 vendor/react-native-gpui/packages/cli/src/commands.test.js create mode 100644 vendor/react-native-gpui/packages/cli/src/desktop-config.js create mode 100644 vendor/react-native-gpui/packages/cli/src/desktop-config.test.js create mode 100644 vendor/react-native-gpui/packages/cli/src/package-desktop.js create mode 100644 vendor/react-native-gpui/packages/cli/src/package-desktop.test.js create mode 100644 vendor/react-native-gpui/packages/cli/src/target-gc.js create mode 100644 vendor/react-native-gpui/packages/cli/src/target-gc.test.js create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/App.tsx create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/babel.config.js create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/.cargo/config.toml create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/README.md create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/assets/README.md create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/assets/icon.png create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/build.rs create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/desktop.json create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/e2e/app.e2e.ts create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/gitignore create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/metro.config.js create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/src/main.rs create mode 100644 vendor/react-native-gpui/packages/cli/templates/app/desktop/vitest.e2e.config.ts create mode 100644 vendor/react-native-gpui/packages/cli/templates/modules/cpp/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/cli/templates/modules/cpp/NativeModule.ts create mode 100644 vendor/react-native-gpui/packages/cli/templates/modules/cpp/RNModule.cpp create mode 100644 vendor/react-native-gpui/packages/cli/templates/modules/cpp/RNModule.hpp create mode 100644 vendor/react-native-gpui/packages/cli/templates/modules/cpp/index.ts create mode 100644 vendor/react-native-gpui/packages/cli/templates/modules/rust/NativeModule.ts create mode 100644 vendor/react-native-gpui/packages/cli/templates/modules/rust/build.rs create mode 100644 vendor/react-native-gpui/packages/cli/templates/modules/rust/index.ts create mode 100644 vendor/react-native-gpui/packages/cli/templates/modules/rust/lib.rs create mode 100644 vendor/react-native-gpui/packages/codegen/package.json create mode 100644 vendor/react-native-gpui/packages/codegen/src/generate-cxx-spec.js create mode 100644 vendor/react-native-gpui/packages/codegen/src/generate-cxx-spec.test.js create mode 100644 vendor/react-native-gpui/packages/codegen/src/generate-host-component-cpp.js create mode 100644 vendor/react-native-gpui/packages/codegen/src/generate-host-component-cpp.test.js create mode 100644 vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.component.test.js create mode 100644 vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.js create mode 100644 vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.test.js create mode 100644 vendor/react-native-gpui/packages/codegen/src/kitchen-sink-spec.js create mode 100644 vendor/react-native-gpui/packages/core/NativeViewHost.tsx create mode 100644 vendor/react-native-gpui/packages/core/NativeViewHostNativeComponent.ts create mode 100644 vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/devsupport/DevServerHelper.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/nativemodule/dom/NativeDOM.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/nativemodule/dom/NativeDOM.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/textinput/BaseTextInputShadowNode.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/view/HostPlatformViewProps.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/view/HostPlatformViewTraitsInitializer.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/textlayoutmanager/platform/cxx/react/renderer/textlayoutmanager/TextLayoutManager.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/AppStateModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/AppStateModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/DeviceInfoModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/PlatformConstantsModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/PlatformConstantsModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevServerHelper.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevServerHelper.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevSettingsModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevSettingsModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/IDevUIDelegate.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/LogBoxModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/LogBoxModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/PackagerConnection.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/SourceCodeModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/Inspector.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/Inspector.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorThread.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IHttpClient.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IHttpClient.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IWebSocketClient.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IWebSocketClient.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/platform/cxx/WebSocketClient.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/platform/cxx/WebSocketClient.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/BlobModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/BlobModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/FileReaderModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/FileReaderModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/IImageLoader.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ImageLoaderModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ImageLoaderModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/NetworkingModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/NetworkingModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ResourceLoader.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ResourceLoader.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/WebSocketModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/WebSocketModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/tests/TestJsRuntime.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/jni/JniHelper.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/jni/JniHelper.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultLogger.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultLogger.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultOnJsErrorHandler.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/LogOnce.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/LogOnce.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/NativeExceptionsManager.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/NativeExceptionsManager.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/JavaScriptModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/TurboModuleProvider.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/FrameStats.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/FrameStats.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/RuntimeHeapInfoCollector.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/RuntimeHeapInfoCollector.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/TimeSeries.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/TimeSeries.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/Tracing.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/perfetto.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/perfetto.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/tests/TimeSeriesTests.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/tracy_noop.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/nativeviewhost/NativeViewHost.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/nativeviewhost/NativeViewHost.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/shortcutscope/ShortcutScope.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/shortcutscope/ShortcutScope.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/windowcontrol/WindowControl.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/windowcontrol/WindowControl.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SurfaceDelegate.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/uimanager/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/uimanager/IMountingManager.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/GpuiDebug.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/PlatformTimerRegistryImpl.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/PlatformTimerRegistryImpl.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactHost.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactHost.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactInstanceConfig.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/MessageQueueThreadImpl.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/MessageQueueThreadImpl.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/TaskDispatchThread.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/TaskDispatchThread.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/tests/.clang-tidy create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/tests/TaskDispatchThreadTests.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/PlatformRunLoopObserver.h create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/RunLoopObserverManager.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/RunLoopObserverManager.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/Cargo.lock create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/Cargo.toml create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/GPUI_PIN.md create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module-build/Cargo.toml create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module-build/src/lib.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/Cargo.toml create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/error.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/host.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/lib.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/private.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/queue.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/registry.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/value.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/Cargo.toml create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/build.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/specs/NativeProbe.ts create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/src/lib.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/module.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/Cargo.toml create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/build.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiAccessibilityMac.mm create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHost.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHost.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponent.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponent.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponentsCpp.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpBridge.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpBridge.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpClient.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpClient.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiImageLoader.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiLogBoxSurfaceDelegate.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiLogBoxSurfaceDelegate.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiModules.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiModules.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiMountingManager.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiMountingManager.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiNodeAddons.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiNodeAddons.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiRustTurboModules.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiRustTurboModules.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiStartupSignpost.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiStartupSignpost.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTextMeasure.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTurboModule.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTurboModule.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/MountBatchBuilder.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/MountBatchBuilder.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/android-shim/README.md create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/android-shim/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/context_menu_view/GpuiContextMenuViewProps.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/image/GpuiImageProps.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/modal/GpuiModalProps.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/scrollview/GpuiScrollViewProps.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/shortcut_scope/GpuiShortcutScopeProps.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/textinput/GpuiTextInputProps.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/window_control/GpuiWindowControlProps.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/DownloadNativeDeps.cmake create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/NodeModules.cmake create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/glog-windows-config.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/modules/PrebuiltInstall.cmake create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/modules/ThirdParty.cmake create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/msvc-preinclude.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/jni/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/rn_gpui_linux_prefix.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/test/gpui_js_engine_test.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/test/gpui_turbo_module_test.cpp create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/ascii_number.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/bigint.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/constexpr_feature_detect.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/decimal_to_binary.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/digit_comparison.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/fast_float.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/fast_table.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/float_common.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/parse_number.h create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/scripts/generate-fbreactnativespec.cjs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/src/lib.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/src/rust_module.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/Cargo.toml create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/build.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/accessibility.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/animation_policy.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/bootstrap.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/alert/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/app_menu/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/clipboard/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/context_menu/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/context_menu_view/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/dev_menu/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/image/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/linking/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/main_thread.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/modal/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/perf_monitor/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/scrollview/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/shortcut_scope/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/shortcuts/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/input.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/render.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/tray/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/window_control/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/windows/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/counters.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/app_menu.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/capabilities.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/client_frame.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/context_menu.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/dev_menu.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/errors.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/gpui_menu.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/icon.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/keymap.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/menu_events.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/menu_model.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/module.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/perf_monitor.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/platform_window.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/tray.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/windows.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/events.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/extensions/input.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/extensions/mod.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/frame.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/host.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/host_component.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/http_client.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/image_loader.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/lib.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/main.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/native_surface.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/native_view.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/native_view_probe.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/node_view.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/pointer_area.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/resources.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/runtime.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/scroll_indicator.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/scroll_layers.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/store.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/style.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/surface.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/test_driver.rs create mode 100644 vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/text.rs create mode 100644 vendor/react-native-gpui/packages/core/babel-plugin-node-addons.js create mode 100644 vendor/react-native-gpui/packages/core/babel-plugin-node-addons.test.js create mode 100644 vendor/react-native-gpui/packages/core/contextMenu.test.js create mode 100644 vendor/react-native-gpui/packages/core/desktop.ts create mode 100644 vendor/react-native-gpui/packages/core/index.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/js-engine.js create mode 100644 vendor/react-native-gpui/packages/core/metro-transform-worker.js create mode 100644 vendor/react-native-gpui/packages/core/metro-transform-worker.test.js create mode 100644 vendor/react-native-gpui/packages/core/metro.config.js create mode 100644 vendor/react-native-gpui/packages/core/metro.config.test.js create mode 100644 vendor/react-native-gpui/packages/core/navigation.ts create mode 100644 vendor/react-native-gpui/packages/core/overrides.json create mode 100644 vendor/react-native-gpui/packages/core/package.json create mode 100644 vendor/react-native-gpui/packages/core/polyfills/menu.tsx create mode 100644 vendor/react-native-gpui/packages/core/prebuilt-binaries.json create mode 100644 vendor/react-native-gpui/packages/core/react-native.config.js create mode 100644 vendor/react-native-gpui/packages/core/scripts/build-prebuilt.mjs create mode 100644 vendor/react-native-gpui/packages/core/scripts/check-prebuilt-pin.mjs create mode 100644 vendor/react-native-gpui/packages/core/scripts/check-standalone-cmake.mjs create mode 100644 vendor/react-native-gpui/packages/core/scripts/generate-prebuilt-inputs.mjs create mode 100644 vendor/react-native-gpui/packages/core/scripts/prebuilt-binaries.mjs create mode 100644 vendor/react-native-gpui/packages/core/scripts/prebuilt-inputs.mjs create mode 100644 vendor/react-native-gpui/packages/core/scripts/prebuilt-inputs.test.mjs create mode 100644 vendor/react-native-gpui/packages/core/scripts/repo-root.mjs create mode 100644 vendor/react-native-gpui/packages/core/setup-env.js create mode 100644 vendor/react-native-gpui/packages/core/shortcuts.test.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Alert/Alert.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Components/Button.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Components/StatusBar/StatusBar.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Components/TextInput/TextInput.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/LogBox/UI/LogBoxButton.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/LogBox/UI/LogBoxInspectorHeader.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/LogBox/UI/LogBoxNotification.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Modal/Modal.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/NativeComponent/BaseViewConfig.desktop.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Network/RCTNetworking.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Utilities/Appearance.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Utilities/BackHandler.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Utilities/Platform.desktop.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/Libraries/Utilities/Platform.desktop.test.js create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/navigation/useDesktopForwardNavigation.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/TitleBar.tsx create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/WindowControl.tsx create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu.tsx create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/ContextMenuHost.tsx create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/MenuRow.tsx create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/MenuSurface.tsx create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/accelerator.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/config.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/levels.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/navigation.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/placement.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/registry.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/safeTriangle.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/theme.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/contextMenu/types.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/index.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/menu.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/perfMonitor.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/shortcuts.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/tray.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/useShortcut.tsx create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/windowContext.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/desktop/windows.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/DesktopContextMenuViewNativeComponent.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/DesktopShortcutScopeNativeComponent.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/DesktopWindowControlNativeComponent.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiAlertManager.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiAppMenu.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiClipboard.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiContextMenu.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiDesktop.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiDevMenu.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiLinkingManager.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiPerfMonitor.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiShortcuts.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiTray.ts create mode 100644 vendor/react-native-gpui/packages/core/src-gpui/src/private/specs/NativeGpuiWindows.ts create mode 100644 vendor/react-native-gpui/packages/core/tsconfig.json create mode 100644 vendor/react-native-gpui/packages/core/types.d.ts create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/AsyncDebuggerAPI.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/CompileJS.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/DebuggerAPI.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/RuntimeTaskRunner.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/SynthTrace.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/SynthTraceParser.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/ThreadSafetyAnalysis.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/TimerStats.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/TraceInterpreter.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/TracingRuntime.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/CDPAgent.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/CDPDebugAPI.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/CallbackOStream.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/ConsoleMessage.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/DebuggerDomainAgent.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/DebuggerDomainCoordinator.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/DomainAgent.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/DomainState.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/HeapProfilerDomainAgent.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/JSONValueInterfaces.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/MessageConverters.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/MessageInterfaces.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/MessageTypes.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/MessageTypesInlines.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/ProfilerDomainAgent.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/RemoteObjectConverters.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/RemoteObjectsTable.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/cdp/RuntimeDomainAgent.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/Dummy.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/Extensions.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/ExtensionsBytecode.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/Intrinsics.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/JSIUtils.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/TextEncoder.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/Worker.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/contrib/ContribDummy.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/contrib/ContribExtensions.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/contrib/TextDecoder.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/extensions/contrib/TextDecoderUtils.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/hermes.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes/hermes_tracing.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes_abi/HermesABIHelpers.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes_abi/HermesABIRuntimeWrapper.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes_abi/hermes_abi.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/hermes_abi/hermes_vtable.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/jsi/jsi/JSIDynamic.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/jsi/jsi/decorator.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/jsi/jsi/hermes-interfaces.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/jsi/jsi/instrumentation.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/jsi/jsi/jsi-inl.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/jsi/jsi/jsi.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/jsi/jsi/jsilib.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/jsi/jsi/test/testlib.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/jsi/jsi/threadsafe.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/napi/hermes_napi.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/napi/hermes_napi_compile.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/napi/hermes_napi_impl.h create mode 100644 vendor/react-native-gpui/packages/hermes/API/napi/hermes_napi_internal.h create mode 100644 vendor/react-native-gpui/packages/hermes/LICENSE create mode 100644 vendor/react-native-gpui/packages/hermes/README.md create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/CMakeLists.txt create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/GpuiHermesEngine.cpp create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/HermesNodeAddons.cpp create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/HermesNodeAddons.h create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/HermesNodeApiHost.cpp create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/HermesNodeApiHost.h create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/bundle.js create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/node-api-exports.test.js create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/node-api-exports.txt create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/runtime-files.js create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/test/hermes_node_api_addon_test.cpp create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/test/hermes_runtime_test.cpp create mode 100644 vendor/react-native-gpui/packages/hermes/desktop/test/node_api_smoke_addon.c create mode 100644 vendor/react-native-gpui/packages/hermes/include/hermes/napi/VENDORED.md create mode 100644 vendor/react-native-gpui/packages/hermes/include/hermes/napi/js_native_api.h create mode 100644 vendor/react-native-gpui/packages/hermes/include/hermes/napi/js_native_api_types.h create mode 100644 vendor/react-native-gpui/packages/hermes/include/hermes/napi/node_api.h create mode 100644 vendor/react-native-gpui/packages/hermes/include/hermes/napi/node_api_types.h create mode 100644 vendor/react-native-gpui/packages/hermes/package.json create mode 100644 vendor/react-native-gpui/packages/hermes/patches/0001-linux-load-icu-at-runtime.patch create mode 100644 vendor/react-native-gpui/packages/hermes/patches/0002-windows-load-node-api-modules.patch create mode 100644 vendor/react-native-gpui/packages/hermes/patches/README.md create mode 100644 vendor/react-native-gpui/packages/hermes/public/hermes/Public/CrashManager.h create mode 100644 vendor/react-native-gpui/packages/hermes/public/hermes/Public/CtorConfig.h create mode 100644 vendor/react-native-gpui/packages/hermes/public/hermes/Public/DebuggerTypes.h create mode 100644 vendor/react-native-gpui/packages/hermes/public/hermes/Public/GCConfig.h create mode 100644 vendor/react-native-gpui/packages/hermes/public/hermes/Public/GCTripwireContext.h create mode 100644 vendor/react-native-gpui/packages/hermes/public/hermes/Public/HermesExport.h create mode 100644 vendor/react-native-gpui/packages/hermes/public/hermes/Public/JSOutOfMemoryError.h create mode 100644 vendor/react-native-gpui/packages/hermes/public/hermes/Public/RuntimeConfig.h create mode 100644 vendor/react-native-gpui/packages/hermes/public/hermes/Public/SamplingProfiler.h create mode 100644 vendor/react-native-gpui/packages/hermes/react-native.config.js create mode 100644 vendor/react-native-gpui/packages/hermes/scripts/fetch-binaries.mjs create mode 100644 vendor/react-native-gpui/packages/hermes/scripts/fetch-binaries.test.mjs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..1fa8a14f7e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +vendor/react-native-gpui/** linguist-vendored +packages/app/gpui/package-lock.json linguist-generated +packages/app/gpui/desktop/Cargo.lock linguist-generated diff --git a/.gitignore b/.gitignore index a85ff1fd8c..a1a1927bd8 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,9 @@ app.json # Project-local Serena configuration and memories. .serena/ + +# Vendored experimental React Native desktop runtime. +!vendor/ +!vendor/react-native-gpui/ +!vendor/react-native-gpui/** +!packages/app/gpui/app.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 5838c7dc1a..68ac400a3e 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -11,5 +11,5 @@ "arrowParens": "always", "bracketSameLine": false, "bracketSpacing": true, - "ignorePatterns": ["*.lock", "**/*.gen.ts", "**/*.gen.tsx"] + "ignorePatterns": ["*.lock", "**/*.gen.ts", "**/*.gen.tsx", "vendor/**"] } diff --git a/.oxlintrc.json b/.oxlintrc.json index c82b517844..3931f35e95 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -3,7 +3,7 @@ "options": { "typeAware": false }, - "ignorePatterns": [".dev/**"], + "ignorePatterns": [".dev/**", "vendor/**"], "plugins": ["react", "react-perf", "unicorn", "typescript", "oxc", "import", "promise"], "jsPlugins": ["./scripts/oxlint/paseo-plugin.mjs"], "categories": { diff --git a/vendor/react-native-gpui/LICENSE b/vendor/react-native-gpui/LICENSE new file mode 100644 index 0000000000..149a0f8321 --- /dev/null +++ b/vendor/react-native-gpui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Streetwriters + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/react-native-gpui/UPSTREAM b/vendor/react-native-gpui/UPSTREAM new file mode 100644 index 0000000000..3822ad2679 --- /dev/null +++ b/vendor/react-native-gpui/UPSTREAM @@ -0,0 +1,21 @@ +Repository: https://github.com/streetwriters/react-native-gpui +Revision: de11aa265e6fb36c80e8a30315bce6d34a9be7d6 +License: MIT; see LICENSE and the licenses retained within packages. + +Snapshot: packages/{cli,codegen,core,hermes} and +modules/{fs,gesture-handler,reanimated,safe-area-context,screens,svg,worklets}. +Examples, tester, testing package, website, and unused fonts/vector-icons ports +are not included. Upstream tests and build sources within retained packages stay +with the snapshot so contributors can inspect and modify the renderer. + +Local patches (all other retained files match the revision above): +- packages/core/ReactGpui/rn-gpui/src/node_view.rs: box-none views must not + install their own pointer area. Otherwise an empty portal overlay redirects + clicks to its parent, blocking Paseo's sidebar. native.test.ts exercises this + against the actual app. +- packages/core/src-gpui/Libraries/Components/StatusBar/StatusBar.desktop.js: + desktop StatusBar implementation, because there is no mobile OS status bar. + +GPUI itself remains a pinned Cargo git dependency. Hermes/React Native prebuilt +libraries are downloaded by upstream build tooling; this is not an offline or +fully source-built distribution. diff --git a/vendor/react-native-gpui/modules/fs/README.md b/vendor/react-native-gpui/modules/fs/README.md new file mode 100644 index 0000000000..b7272a43c9 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/README.md @@ -0,0 +1,3 @@ +# fs + +Rust TurboModule library for React Native GPUI. The spec is in src/NativeFs.ts and uses React Native's public TurboModuleRegistry API. The implementation is in desktop/src/lib.rs and is discovered through package metadata. Import the default export from @react-native-gpui/fs and call its typed greet(name) method. diff --git a/vendor/react-native-gpui/modules/fs/desktop/Cargo.toml b/vendor/react-native-gpui/modules/fs/desktop/Cargo.toml new file mode 100644 index 0000000000..41b80cfa87 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "rn-gpui-fs" +version = "0.1.0" +edition = "2021" + +[dependencies] +rn-gpui-module = { version = "0.1" } +# Dialogs with file-type filters and a default name on all three platforms, using +# the XDG portal on Linux so no GTK is required. +rfd = "0.15" +# The standard cross-platform watcher. +notify = "6" +# Well-known folders, which differ per platform in ways not worth re-deriving. +dirs = "5" +# `utimes` sets both timestamps of an existing file; `std` cannot. +filetime = "0.2" +# `mkdtemp` with Node's retry-on-collision behaviour. +tempfile = "3" +# `chmod` by descriptor, and the errno constants the error mapping reads. +libc = "0.2" +# `realpath` without the `\\?\` prefix on Windows. +dunce = "1" + +[build-dependencies] +rn-gpui-module-build = { version = "0.1" } \ No newline at end of file diff --git a/vendor/react-native-gpui/modules/fs/desktop/build.rs b/vendor/react-native-gpui/modules/fs/desktop/build.rs new file mode 100644 index 0000000000..dc70db9bb3 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/build.rs @@ -0,0 +1,3 @@ +fn main() { + rn_gpui_module_build::generate(&["../src/NativeFs.ts"]); +} diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/dialogs.rs b/vendor/react-native-gpui/modules/fs/desktop/src/dialogs.rs new file mode 100644 index 0000000000..caac6cd206 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/dialogs.rs @@ -0,0 +1,394 @@ +//! The native pickers, Finder/Explorer reveal, and "open with the default app". +//! +//! `rfd` owns the dialogs, so the platform differences (AppKit panels, the +//! Windows common dialogs, the XDG portal on Linux) stay out of this crate. All +//! of `rfd`'s dialog entry points are blocking, which is acceptable here: the +//! module has a thread of its own (`THREAD = Own`), so a panel left open blocks +//! neither JS nor a frame. +//! +//! # Open question: dialogs on macOS and the main thread +//! +//! The design flags that macOS may require the panel to be created on the main +//! thread, and that this has to be confirmed under GPUI's run loop before it can +//! be called done. What `rfd` 0.15's blocking API actually does on macOS is +//! dispatch the panel onto the main queue when the process already has a running +//! `NSApplication`, run it inline when the caller is already on the main thread, +//! and panic when neither holds. This module is on its own thread, so the first +//! branch is the one that applies, and the call should hop to the main thread and +//! block this module's thread until the panel closes. +//! +//! That is the theory, and it is **not yet verified** under GPUI's run loop. The +//! spike is: run the app, open a dialog from JS, and check that the panel +//! appears, that no main-thread warning is logged, that the module thread +//! resumes when it closes, and that the app does not deadlock while the panel is +//! up. If any of that fails, the fallback is the one the design names: a second +//! module, `GpuiFsDialogs`, with `THREAD = Main`, holding only the two pickers; +//! the rest of this module stays off the main thread. Attaching the panel to the +//! calling window (a sheet on macOS) is a further step that needs a host +//! function exposing the active window's raw handle for `rfd`'s `set_parent`, +//! which does not exist yet either. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use rn_gpui_module::ModuleError; + +use crate::errors; +use crate::{DialogFilter, OpenDialogOptions, SaveDialogOptions}; + +/// The open picker. An empty list means the user cancelled, which is what the JS +/// layer expects; a cancelled dialog is not a failure. +pub fn show_open(options: OpenDialogOptions) -> Result, ModuleError> { + let dialog = configured( + options.title.as_deref(), + options.default_path.as_deref(), + &options.filters, + ); + + let picked = if options.directory { + // A folder picker cannot return files, and `pick_folder` selects a + // single folder — rfd's blocking folder call has no multi-select mode. + // So `multiple` is honoured only as far as the platform allows: a + // caller asking for `{ directory: true, multiple: true }` gets at most + // one path, which is better than refusing the call outright. + dialog.pick_folder().into_iter().collect() + } else if options.multiple { + dialog.pick_files().unwrap_or_default() + } else { + dialog.pick_file().into_iter().collect() + }; + + Ok(utf8_paths(picked)) +} + +/// The save picker. `None` means the user cancelled; no file is created here +/// either way, the caller decides what to write. +pub fn show_save(options: SaveDialogOptions) -> Result, ModuleError> { + let (_, from_path) = split_default_path(options.default_path.as_deref()); + // `defaultName` wins over the name implied by `defaultPath`, but a path + // ending in a file name is still the only hint a caller may give. + let name = options + .default_name + .filter(|name| !name.trim().is_empty()) + .or(from_path); + + let mut dialog = configured( + options.title.as_deref(), + options.default_path.as_deref(), + &options.filters, + ); + if let Some(name) = name { + dialog = dialog.set_file_name(name); + } + + let picked = dialog.save_file(); + // The picker never writes, so a chosen name the bridge cannot represent is + // reported as "nothing chosen" rather than as an error the user has no way + // to act on; the alternative is a file the caller would try to write at a + // path JS could not have named. + Ok(picked.as_deref().and_then(utf8_path)) +} + +/// Shows `path` in the platform's file manager, selected where the platform has +/// a verb for it. +pub fn reveal(path: String) -> Result<(), ModuleError> { + ensure_exists(&path)?; + run(reveal_command(Path::new(&path)), &path) +} + +/// Opens `path` with whatever the platform associates with it. +pub fn open_external(path: String) -> Result<(), ModuleError> { + ensure_exists(&path)?; + run(open_external_command(Path::new(&path)), &path) +} + +/// A dialog carrying everything the three calls configure the same way: title, +/// starting directory, and the extension filters. +fn configured( + title: Option<&str>, + default_path: Option<&str>, + filters: &[DialogFilter], +) -> rfd::FileDialog { + let mut dialog = rfd::FileDialog::new(); + if let Some(title) = title { + dialog = dialog.set_title(title); + } + if let Some(directory) = split_default_path(default_path).0 { + dialog = dialog.set_directory(directory); + } + for filter in filters { + // An empty extension list carries no information, and on some backends + // a filter is applied as an allow-list, which would hide every file. + if !filter.extensions.is_empty() { + dialog = dialog.add_filter(filter.name.as_str(), &filter.extensions); + } + } + dialog +} + +/// Splits `defaultPath` into the directory a picker should open in, and the file +/// name to prefill when the path names a file rather than a folder. +/// +/// The two pickers need different halves: the open dialog only starts in the +/// directory, the save dialog also offers the name. +fn split_default_path(default_path: Option<&str>) -> (Option, Option) { + let Some(raw) = default_path.map(str::trim).filter(|path| !path.is_empty()) else { + return (None, None); + }; + let path = Path::new(raw); + + let is_directory = match std::fs::metadata(path) { + Ok(metadata) => metadata.is_dir(), + // A save dialog is usually given a path that does not exist yet, so fall + // back to the spelling: a trailing separator promises a directory, and + // anything else is a proposed file name whose parent is the start. + Err(_) => ends_with_separator(raw), + }; + + if is_directory { + (Some(path.to_path_buf()), None) + } else { + ( + path.parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .map(Path::to_path_buf), + path.file_name() + .and_then(|name| name.to_str()) + .map(str::to_string), + ) + } +} + +/// Whether a path is spelled as a directory on any of the platforms this module +/// runs on, so a Windows path still splits correctly when read elsewhere. +fn ends_with_separator(path: &str) -> bool { + path.ends_with('/') || path.ends_with('\\') +} + +/// The picked paths as strings, dropping any the bridge cannot represent. +/// +/// A Unix file name need not be UTF-8, but JS can only hold strings. Failing the +/// whole selection because one of several files has an unusual name would throw +/// away the files the user can use, so the others are returned instead. +fn utf8_paths(paths: Vec) -> Vec { + paths + .into_iter() + .filter_map(|path| utf8_path(&path)) + .collect() +} + +fn utf8_path(path: &Path) -> Option { + path.to_str().map(str::to_string) +} + +/// Fails with the same code `stat` would before a platform command can turn the +/// problem into an opaque non-zero exit. +fn ensure_exists(path: &str) -> Result<(), ModuleError> { + // `symlink_metadata` rather than `metadata`: a dangling symlink is still a + // row in Finder or Explorer, so revealing it is meaningful. + std::fs::symlink_metadata(path) + .map(|_| ()) + .map_err(|error| errors::from_io(error, path)) +} + +/// A command line, kept separate from running it so the platform branches can be +/// asserted on without opening anything. +#[derive(Debug, Clone, PartialEq, Eq)] +struct CommandSpec { + program: &'static str, + args: Vec, +} + +/// The command that reveals `path` in the platform's file manager. +fn reveal_command(path: &Path) -> CommandSpec { + let shown = path.to_string_lossy().into_owned(); + + #[cfg(target_os = "macos")] + { + // `open -R` is the only thing that selects the item rather than opening + // its containing folder. + CommandSpec { + program: "open", + args: vec!["-R".to_string(), shown], + } + } + + #[cfg(target_os = "windows")] + { + // `/select,` is a single argument, comma included. + // + // Note: explorer.exe is known to return exit code 1 even when it does + // open the window, which the non-zero check below would report as a + // failure. That needs a check on a Windows machine; if it does bite, + // this branch should treat a successful spawn as success instead of + // reading the exit code. + CommandSpec { + program: "explorer", + args: vec![format!("/select,{shown}")], + } + } + + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + // `xdg-open` has no portable "select this item" verb, so the containing + // folder is the closest equivalent. A path with no parent is its own + // directory and is opened as it is. + let folder = path.parent().unwrap_or(path); + CommandSpec { + program: "xdg-open", + args: vec![folder.to_string_lossy().into_owned()], + } + } +} + +/// The command that opens `path` with the platform's default application. +fn open_external_command(path: &Path) -> CommandSpec { + let shown = path.to_string_lossy().into_owned(); + + #[cfg(target_os = "macos")] + { + CommandSpec { + program: "open", + args: vec![shown], + } + } + + #[cfg(target_os = "windows")] + { + // `start` reads its first quoted argument as the window title, so the + // empty string is required for a path containing spaces to survive. + CommandSpec { + program: "cmd", + args: vec!["/C".to_string(), "start".to_string(), String::new(), shown], + } + } + + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + CommandSpec { + program: "xdg-open", + args: vec![shown], + } + } +} + +/// Runs a command and turns a refusal into a Node-shaped error. +/// +/// `Command` is used without a shell, so a path is one argument no matter what +/// characters it contains; quoting and escaping are the platform's problem, not +/// a shell's. +fn run(spec: CommandSpec, path: &str) -> Result<(), ModuleError> { + let status = Command::new(spec.program) + .args(&spec.args) + .status() + // The program itself could not be started (missing, or not executable), + // which is an I/O failure on the path the caller asked about. + .map_err(|error| errors::from_io(error, path))?; + + if status.success() { + return Ok(()); + } + Err(errors::invalid( + "EIO", + format!( + "EIO: i/o error, {path} ({} exited with {status})", + spec.program + ), + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_default_path_that_names_a_file_splits_into_parent_and_name() { + let file = std::env::temp_dir().join("rn-gpui-fs-dialog-default.txt"); + let (directory, name) = split_default_path(Some(&file.to_string_lossy())); + assert_eq!(directory.as_deref(), Some(std::env::temp_dir().as_path())); + assert_eq!(name.as_deref(), Some("rn-gpui-fs-dialog-default.txt")); + } + + #[test] + fn a_default_path_that_names_an_existing_directory_stays_a_directory() { + let directory = std::env::temp_dir(); + let (found, name) = split_default_path(Some(&directory.to_string_lossy())); + assert_eq!(found, Some(directory)); + assert_eq!(name, None); + } + + #[test] + fn a_bare_file_name_has_no_directory_to_open_in() { + let (directory, name) = split_default_path(Some("rn-gpui-fs-report.csv")); + assert_eq!(directory, None); + assert_eq!(name.as_deref(), Some("rn-gpui-fs-report.csv")); + } + + #[test] + fn a_trailing_separator_marks_a_path_that_does_not_exist_as_a_directory() { + let missing = std::env::temp_dir().join("rn-gpui-fs-not-created"); + let spelled = format!("{}/", missing.to_string_lossy()); + let (directory, name) = split_default_path(Some(&spelled)); + assert_eq!(directory, Some(missing)); + assert_eq!(name, None); + } + + #[test] + fn no_default_path_means_no_directory_and_no_name() { + assert_eq!(split_default_path(None), (None, None)); + assert_eq!(split_default_path(Some(" ")), (None, None)); + } + + #[test] + fn reveal_targets_the_platform_file_manager() { + let spec = reveal_command(Path::new("/tmp/example.txt")); + #[cfg(target_os = "macos")] + { + assert_eq!(spec.program, "open"); + assert_eq!(spec.args, ["-R", "/tmp/example.txt"]); + } + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + assert_eq!(spec.program, "xdg-open"); + // Only the folder, because xdg-open cannot select an item in it. + assert_eq!(spec.args, ["/tmp"]); + } + #[cfg(target_os = "windows")] + { + assert_eq!(spec.program, "explorer"); + assert_eq!(spec.args, ["/select,/tmp/example.txt"]); + } + } + + #[test] + fn open_external_hands_the_path_to_the_default_application() { + let spec = open_external_command(Path::new("/tmp/example.txt")); + assert_eq!( + spec.args.last().map(String::as_str), + Some("/tmp/example.txt") + ); + #[cfg(target_os = "macos")] + { + assert_eq!(spec.program, "open"); + assert_eq!(spec.args.len(), 1); + } + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + assert_eq!(spec.program, "xdg-open"); + #[cfg(target_os = "windows")] + { + assert_eq!(spec.program, "cmd"); + // `/C start "" `: the empty argument is the title `start` + // would otherwise take the path for. + assert_eq!(&spec.args[..3], ["/C", "start", ""]); + } + } + + #[test] + fn a_path_with_spaces_is_still_a_single_argument() { + let spec = open_external_command(Path::new("/tmp/two words.txt")); + assert_eq!( + spec.args.last().map(String::as_str), + Some("/tmp/two words.txt") + ); + } +} diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/dirs.rs b/vendor/react-native-gpui/modules/fs/desktop/src/dirs.rs new file mode 100644 index 0000000000..251c2967bd --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/dirs.rs @@ -0,0 +1,363 @@ +//! Listing, creating and removing directories, and `mkdtemp`. +//! +//! `readdir` returns every entry as a plain `{name, kind, parentPath}` object, +//! which the JS layer turns into `Dirent` instances; there is no per-directory +//! cursor to hold open because the whole listing crosses the bridge at once. + +use std::fs; +use std::path::{Path, PathBuf}; + +use rn_gpui_module::ModuleError; + +use crate::errors; +use crate::module::kind; +use crate::DirEntry; + +pub async fn readdir(path: String, recursive: bool) -> Result, ModuleError> { + readdir_sync(path, recursive) +} + +pub async fn mkdir(path: String, recursive: bool, mode: f64) -> Result<(), ModuleError> { + create_dir(&path, recursive, mode) +} + +pub async fn rm(path: String, recursive: bool, force: bool) -> Result<(), ModuleError> { + remove(&path, recursive, force) +} + +pub async fn mkdtemp(prefix: String) -> Result { + let directory = tempfile::Builder::new() + .prefix(&prefix) + .tempdir_in(std::env::temp_dir()) + .map_err(|error| errors::from_io(error, &prefix))?; + // The caller asked for a directory to keep, so it must outlive this frame; + // dropping the guard would delete it again. + Ok(path_string(directory.keep())) +} + +pub fn readdir_sync(path: String, recursive: bool) -> Result, ModuleError> { + let entries = list(Path::new(&path))?; + if !recursive { + return Ok(entries); + } + let mut collected = entries; + // Depth-first, appending each level as it is reached. Iterative rather than + // recursive: a deep tree should cost heap, not stack. + let mut pending: Vec = collected + .iter() + .filter(|entry| entry.kind == kind::DIRECTORY) + .map(|entry| Path::new(&entry.parent_path).join(&entry.name)) + .collect(); + while let Some(directory) = pending.pop() { + let children = list(&directory)?; + pending.extend( + children + .iter() + .filter(|entry| entry.kind == kind::DIRECTORY) + .map(|entry| Path::new(&entry.parent_path).join(&entry.name)), + ); + collected.extend(children); + } + Ok(collected) +} + +pub fn mkdir_sync(path: String, recursive: bool, mode: f64) -> Result { + create_dir(&path, recursive, mode)?; + Ok(true) +} + +pub fn rm_sync(path: String, recursive: bool, force: bool) -> Result { + remove(&path, recursive, force)?; + Ok(true) +} + +/// The direct children of `path`, in the order the file system lists them. +/// +/// `DirEntry::file_type` does not follow symbolic links, so a link is reported +/// as a link rather than as whatever it points at. +fn list(path: &Path) -> Result, ModuleError> { + let name = path_string(path.to_path_buf()); + let iterator = fs::read_dir(path).map_err(|error| errors::from_io(error, &name))?; + let mut entries = Vec::new(); + for entry in iterator { + let entry = entry.map_err(|error| errors::from_io(error, &name))?; + let file_type = entry + .file_type() + .map_err(|error| errors::from_io(error, &name))?; + entries.push(DirEntry { + name: entry.file_name().to_string_lossy().into_owned(), + kind: entry_kind(&file_type), + parent_path: name.clone(), + }); + } + Ok(entries) +} + +fn entry_kind(file_type: &fs::FileType) -> f64 { + if file_type.is_file() { + kind::FILE + } else if file_type.is_dir() { + kind::DIRECTORY + } else if file_type.is_symlink() { + kind::SYMLINK + } else { + kind::OTHER + } +} + +fn create_dir(path: &str, recursive: bool, mode: f64) -> Result<(), ModuleError> { + if recursive { + // `create_dir_all` succeeds on an existing directory, which is what + // Node's `{recursive: true}` promises. + return fs::create_dir_all(path).map_err(|error| errors::from_io(error, path)); + } + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt as _; + let mut builder = fs::DirBuilder::new(); + builder.mode(crate::module::mode(mode)); + builder + .create(path) + .map_err(|error| errors::from_io(error, path)) + } + // Windows has no mode for a new directory, and `create_dir` would reject + // the `DirBuilder` extension that only exists on Unix. + #[cfg(not(unix))] + { + let _ = mode; + fs::create_dir(path).map_err(|error| errors::from_io(error, path)) + } +} + +fn remove(path: &str, recursive: bool, force: bool) -> Result<(), ModuleError> { + // `symlink_metadata`, so a link is removed as a link instead of acting on + // its target. + let metadata = match fs::symlink_metadata(path) { + Ok(metadata) => metadata, + Err(error) if force && errors::code_for(&error) == "ENOENT" => return Ok(()), + Err(error) => return Err(errors::from_io(error, path)), + }; + if recursive && metadata.is_dir() { + return fs::remove_dir_all(path).map_err(|error| errors::from_io(error, path)); + } + if metadata.is_dir() { + // `remove_dir` fails with ENOTEMPTY on Unix and a folder-not-empty error + // on Windows, which the mapping turns into the same code; removing a + // file with it reports what Node calls ENOTDIR. + fs::remove_dir(path).map_err(|error| errors::from_io(error, path)) + } else { + fs::remove_file(path).map_err(|error| errors::from_io(error, path)) + } +} + +fn path_string(path: PathBuf) -> String { + path.to_string_lossy().into_owned() +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + /// The async wrappers never suspend, so polling once with a no-op waker is + /// enough and keeps the tests independent of a runtime. + fn block_on(future: F) -> F::Output { + let mut future = std::pin::pin!(future); + let waker = std::task::Waker::noop(); + let mut context = std::task::Context::from_waker(waker); + // One poll, no loop: these helpers do their work inline, so a `Pending` + // would mean the helper is not synchronous after all, which the test + // should fail loudly on rather than spin. + match future.as_mut().poll(&mut context) { + std::task::Poll::Ready(value) => value, + std::task::Poll::Pending => panic!("the wrappers are synchronous"), + } + } + + fn tempdir() -> TempDir { + TempDir::new().expect("a temporary directory") + } + + fn text(path: &Path) -> String { + path.to_string_lossy().into_owned() + } + + /// The listed paths, with separators normalised to `/`. + /// + /// The entries themselves carry the platform separator, which on Windows is + /// `\`; building the expected path with `/` and comparing would test the + /// separator rather than the listing. + fn names(entries: &[DirEntry]) -> Vec { + let mut names: Vec = entries + .iter() + .map(|entry| format!("{}/{}", entry.parent_path, entry.name).replace('\\', "/")) + .collect(); + names.sort(); + names + } + + fn tree(root: &Path) { + fs::create_dir_all(root.join("nested").join("deep")).expect("nested"); + fs::write(root.join("top.txt"), b"top").expect("seed"); + fs::write(root.join("nested").join("middle.txt"), b"middle").expect("seed"); + fs::write(root.join("nested").join("deep").join("leaf.txt"), b"leaf").expect("seed"); + } + + #[test] + fn readdir_lists_direct_children_only() { + let dir = tempdir(); + tree(dir.path()); + + let entries = readdir_sync(text(dir.path()), false).expect("readdir"); + // `names` normalises separators, so the expected paths are built from a + // normalised root rather than the platform one. + let root = text(dir.path()).replace('\\', "/"); + assert_eq!( + names(&entries), + vec![format!("{root}/nested"), format!("{root}/top.txt")] + ); + + let top = entries + .iter() + .find(|entry| entry.name == "top.txt") + .expect("top.txt"); + assert_eq!(top.kind, kind::FILE); + assert_eq!(top.parent_path, text(dir.path())); + let nested = entries + .iter() + .find(|entry| entry.name == "nested") + .expect("nested"); + assert_eq!(nested.kind, kind::DIRECTORY); + } + + #[test] + fn readdir_recursive_reaches_every_descendant() { + let dir = tempdir(); + tree(dir.path()); + + let entries = block_on(readdir(text(dir.path()), true)).expect("readdir"); + // Two entries per directory, three directories deep. + assert_eq!(entries.len(), 5); + let listed = names(&entries); + let root = text(dir.path()).replace('\\', "/"); + assert!(listed.contains(&format!("{root}/nested/deep/leaf.txt"))); + assert!(listed.contains(&format!("{root}/nested/deep"))); + + let deep = entries + .iter() + .find(|entry| entry.name == "deep") + .expect("deep"); + assert_eq!( + deep.parent_path, + text(&dir.path().join("nested")), + "parentPath is the containing directory" + ); + } + + #[cfg(unix)] + #[test] + fn readdir_reports_a_link_as_a_link() { + let dir = tempdir(); + let target = dir.path().join("target.txt"); + fs::write(&target, b"x").expect("seed"); + std::os::unix::fs::symlink(&target, dir.path().join("link.txt")).expect("symlink"); + + let entries = readdir_sync(text(dir.path()), false).expect("readdir"); + let link = entries + .iter() + .find(|entry| entry.name == "link.txt") + .expect("link.txt"); + assert_eq!(link.kind, kind::SYMLINK); + } + + #[test] + fn mkdir_with_recursive_creates_the_chain_and_accepts_an_existing_dir() { + let dir = tempdir(); + let nested = dir.path().join("a").join("b").join("c"); + + block_on(mkdir(text(&nested), true, f64::from(0o755u32))).expect("mkdir"); + assert!(nested.is_dir()); + + let again = mkdir_sync(text(&nested), true, f64::from(0o755u32)).expect("mkdir again"); + assert!(again, "recursive mkdir succeeds when the path exists"); + } + + #[test] + fn mkdir_without_recursive_fails_with_eexist() { + let dir = tempdir(); + let path = dir.path().join("single"); + assert!(mkdir_sync(text(&path), false, f64::from(0o755u32)).expect("mkdir")); + assert!(path.is_dir()); + + let error = + block_on(mkdir(text(&path), false, f64::from(0o755u32))).expect_err("mkdir twice"); + assert_eq!(error.code.as_deref(), Some("EEXIST")); + + let missing_parent = text(&dir.path().join("no").join("such").join("dir")); + let error = + mkdir_sync(missing_parent, false, f64::from(0o755u32)).expect_err("missing parent"); + assert_eq!(error.code.as_deref(), Some("ENOENT")); + } + + #[test] + fn rm_force_ignores_a_missing_path() { + let dir = tempdir(); + let missing = text(&dir.path().join("absent")); + + block_on(rm(missing.clone(), false, true)).expect("rm force"); + assert!(rm_sync(missing.clone(), true, true).expect("rm sync force")); + + let error = block_on(rm(missing, false, false)).expect_err("rm without force"); + assert_eq!(error.code.as_deref(), Some("ENOENT")); + } + + #[test] + fn rm_removes_a_file_and_a_tree() { + let dir = tempdir(); + let file = dir.path().join("one.txt"); + fs::write(&file, b"x").expect("seed"); + block_on(rm(text(&file), false, false)).expect("rm file"); + assert!(!file.exists()); + + let root = dir.path().join("tree"); + tree(&root); + assert!(rm_sync(text(&root), true, false).expect("rm tree")); + assert!(!root.exists()); + } + + #[test] + fn rm_without_recursive_refuses_a_non_empty_directory() { + let dir = tempdir(); + let root = dir.path().join("tree"); + tree(&root); + + let error = block_on(rm(text(&root), false, false)).expect_err("non-empty"); + assert_eq!(error.code.as_deref(), Some("ENOTEMPTY")); + assert!(root.exists()); + } + + #[test] + fn mkdtemp_creates_a_random_directory_with_the_prefix() { + let created = block_on(mkdtemp("rn-gpui-fs-".to_string())).expect("mkdtemp"); + let path = Path::new(&created); + let name = path + .file_name() + .expect("the returned path names a directory") + .to_string_lossy() + .into_owned(); + + assert!(name.starts_with("rn-gpui-fs-"), "name was {name}"); + assert!(name.len() > "rn-gpui-fs-".len(), "the rest is random"); + assert!(path.is_dir()); + assert_eq!(path.parent(), Some(std::env::temp_dir().as_path())); + + fs::remove_dir_all(path).expect("clean up"); + } + + #[test] + fn readdir_of_a_missing_directory_is_enoent() { + let dir = tempdir(); + let error = readdir_sync(text(&dir.path().join("absent")), false).expect_err("readdir"); + assert_eq!(error.code.as_deref(), Some("ENOENT")); + } +} diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/errors.rs b/vendor/react-native-gpui/modules/fs/desktop/src/errors.rs new file mode 100644 index 0000000000..c31537a877 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/errors.rs @@ -0,0 +1,149 @@ +//! Node-compatible error mapping. +//! +//! Every failure a caller sees has to look like the one Node would have raised for +//! the same call: a `code` such as `ENOENT`, and a message in Node's format. +//! `std::io::Error` gives a kind and, on Unix, a raw errno; neither is a Node code, +//! so the translation lives here once rather than at each call site. + +use rn_gpui_module::ModuleError; + +/// The Node code for an `std::io::Error`. +/// +/// The raw OS error is consulted before the kind because several distinct errnos +/// share a kind: `EEXIST` and `ENOTEMPTY` are both "already exists"-ish, and +/// `EACCES` versus `EPERM` matter to a caller deciding whether to try again. +pub fn code_for(error: &std::io::Error) -> &'static str { + #[cfg(unix)] + if let Some(errno) = error.raw_os_error() { + let code = match errno { + libc::ENOENT => "ENOENT", + libc::EACCES => "EACCES", + libc::EPERM => "EPERM", + libc::EEXIST => "EEXIST", + libc::ENOTDIR => "ENOTDIR", + libc::EISDIR => "EISDIR", + libc::ENOTEMPTY => "ENOTEMPTY", + libc::EINVAL => "EINVAL", + libc::ELOOP => "ELOOP", + libc::ENOSPC => "ENOSPC", + libc::EROFS => "EROFS", + libc::EMFILE => "EMFILE", + libc::ENAMETOOLONG => "ENAMETOOLONG", + // Windows' share-violation has no Unix equivalent; callers treat it + // as a permission problem, which is what Node's `EPERM` means there. + _ => return code_for_kind(error.kind()), + }; + return code; + } + // Windows exposes `raw_os_error` too, but its values are Win32 codes rather + // than errnos, so the kind is the better signal there, with the few Win32 + // codes whose kind is not enough named below. + #[cfg(not(unix))] + if let Some(code) = win32_code(error.raw_os_error()) { + return code; + } + code_for_kind(error.kind()) +} + +/// The Node code for a Win32 error whose `ErrorKind` is too coarse. +/// +/// `ERROR_PRIVILEGE_NOT_HELD` (1314) has no kind of its own, so it would be `EIO`. +/// Creating a symbolic link without the privilege is the case that hits it, and +/// libuv, which Node is built on, reports it as `EPERM`. +#[cfg(not(unix))] +fn win32_code(raw: Option) -> Option<&'static str> { + match raw { + Some(1314) => Some("EPERM"), + _ => None, + } +} + +fn code_for_kind(kind: std::io::ErrorKind) -> &'static str { + use std::io::ErrorKind; + match kind { + ErrorKind::NotFound => "ENOENT", + ErrorKind::PermissionDenied => "EACCES", + ErrorKind::AlreadyExists => "EEXIST", + ErrorKind::InvalidInput => "EINVAL", + ErrorKind::InvalidData => "EINVAL", + ErrorKind::WriteZero => "ENOSPC", + ErrorKind::StorageFull => "ENOSPC", + ErrorKind::ReadOnlyFilesystem => "EROFS", + ErrorKind::DirectoryNotEmpty => "ENOTEMPTY", + ErrorKind::IsADirectory => "EISDIR", + ErrorKind::NotADirectory => "ENOTDIR", + _ => "EIO", + } +} + +/// The human half of a Node error message, by code. +/// +/// Node's own table, because the tests compare against messages and because these +/// strings are what a developer recognises when a path is wrong. +fn description_for(code: &str) -> &'static str { + match code { + "ENOENT" => "no such file or directory", + "EACCES" => "permission denied", + "EPERM" => "operation not permitted", + "EEXIST" => "file already exists", + "ENOTDIR" => "not a directory", + "EISDIR" => "illegal operation on a directory", + "ENOTEMPTY" => "directory not empty", + "EINVAL" => "invalid argument", + "ELOOP" => "too many symbolic links encountered", + "ENOSPC" => "no space left on device", + "EROFS" => "read-only file system", + "EMFILE" => "too many open files", + "ENAMETOOLONG" => "name too long", + _ => "i/o error", + } +} + +/// An `std::io::Error` as the `ModuleError` JS should receive. +/// +/// `syscall` and `path` are attached by the JS layer, which knows the names the +/// caller used; the native side would have to guess between `open`, `lstat` and +/// the rest for what is already a single `stat` call. +pub fn from_io(error: std::io::Error, path: &str) -> ModuleError { + let code = code_for(&error); + ModuleError::with_code(code, format!("{code}: {}, {path}", description_for(code))) +} + +/// A failure that has no `std::io::Error`, such as a closed file descriptor. +pub fn invalid(code: &str, message: impl Into) -> ModuleError { + ModuleError::with_code(code, message) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_missing_file_is_enoent_with_nodes_message() { + let error = std::io::Error::new(std::io::ErrorKind::NotFound, "gone"); + let mapped = from_io(error, "/nope"); + assert_eq!(mapped.code.as_deref(), Some("ENOENT")); + assert_eq!(mapped.message, "ENOENT: no such file or directory, /nope"); + } + + #[test] + fn a_denied_file_is_eacces() { + let error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "nope"); + assert_eq!(from_io(error, "/x").code.as_deref(), Some("EACCES")); + } + + #[cfg(windows)] + #[test] + fn a_missing_symlink_privilege_is_eperm_not_eio() { + // ERROR_PRIVILEGE_NOT_HELD: what `symlink_dir` returns without developer + // mode, and what Node reports as EPERM. + let error = std::io::Error::from_raw_os_error(1314); + assert_eq!(from_io(error, "C:\\link").code.as_deref(), Some("EPERM")); + } + + #[test] + fn an_unknown_failure_still_has_a_code() { + let error = std::io::Error::new(std::io::ErrorKind::Other, "odd"); + assert_eq!(from_io(error, "/x").code.as_deref(), Some("EIO")); + } +} diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/files.rs b/vendor/react-native-gpui/modules/fs/desktop/src/files.rs new file mode 100644 index 0000000000..bef854b172 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/files.rs @@ -0,0 +1,640 @@ +//! Reading, writing, copying, truncating, permissions and times. +//! +//! The async forms are thin wrappers over the sync ones: `std::fs` is blocking +//! either way, and this module has a thread of its own, so the only difference +//! is which thread the bridge calls the function on. +//! +//! Every failure goes through `errors::from_io`, so JS receives a Node code and +//! Node's message rather than an `std::io::Error` kind. + +use std::fs; +use std::io::Write as _; +use std::path::Path; + +use rn_gpui_module::{ArrayBuffer, ModuleError}; + +use crate::errors; +use crate::module::{kind, millis, open_options, system_time}; +use crate::StatResult; + +pub async fn read_file(path: String) -> Result { + read_file_sync(path) +} + +pub async fn read_text_file(path: String) -> Result { + read_text_file_sync(path) +} + +pub async fn write_file( + path: String, + data: ArrayBuffer, + flags: f64, + mode: f64, +) -> Result<(), ModuleError> { + write_bytes(&path, data.as_slice(), flags, mode) +} + +pub async fn write_text_file( + path: String, + data: String, + flags: f64, + mode: f64, +) -> Result<(), ModuleError> { + write_bytes(&path, data.as_bytes(), flags, mode) +} + +pub async fn stat(path: String, follow_symlinks: bool) -> Result { + stat_of(&path, follow_symlinks) +} + +pub async fn rename(from: String, to: String) -> Result<(), ModuleError> { + fs::rename(&from, &to).map_err(|error| errors::from_io(error, &from)) +} + +pub async fn copy(from: String, to: String, recursive: bool) -> Result<(), ModuleError> { + copy_path(&from, &to, recursive) +} + +pub async fn truncate(path: String, length: f64) -> Result<(), ModuleError> { + let length = crate::module::length(length)?; + let file = fs::OpenOptions::new() + .write(true) + .open(&path) + .map_err(|error| errors::from_io(error, &path))?; + file.set_len(length as u64) + .map_err(|error| errors::from_io(error, &path)) +} + +pub async fn utimes(path: String, atime_ms: f64, mtime_ms: f64) -> Result<(), ModuleError> { + let atime = filetime::FileTime::from_system_time(system_time(atime_ms)); + let mtime = filetime::FileTime::from_system_time(system_time(mtime_ms)); + filetime::set_file_times(&path, atime, mtime).map_err(|error| errors::from_io(error, &path)) +} + +pub async fn chmod(path: String, mode: f64) -> Result<(), ModuleError> { + set_mode(&path, mode) +} + +pub fn read_file_sync(path: String) -> Result { + fs::read(&path) + .map(ArrayBuffer::from) + .map_err(|error| errors::from_io(error, &path)) +} + +pub fn read_text_file_sync(path: String) -> Result { + let bytes = fs::read(&path).map_err(|error| errors::from_io(error, &path))?; + // Node decodes with replacement characters instead of rejecting a file that + // is not valid UTF-8, so reading a mis-encoded file still yields text. + Ok(String::from_utf8_lossy(&bytes).into_owned()) +} + +pub fn write_file_sync( + path: String, + data: ArrayBuffer, + flags: f64, + mode: f64, +) -> Result { + write_bytes(&path, data.as_slice(), flags, mode)?; + Ok(true) +} + +pub fn write_text_file_sync( + path: String, + data: String, + flags: f64, + mode: f64, +) -> Result { + write_bytes(&path, data.as_bytes(), flags, mode)?; + Ok(true) +} + +/// `Ok(None)` for a path that is not there: that is what `existsSync` asks, and +/// only a missing path means "no". A permission failure is still an error. +pub fn stat_sync(path: String, follow_symlinks: bool) -> Result, ModuleError> { + match stat_of(&path, follow_symlinks) { + Ok(result) => Ok(Some(result)), + Err(error) if error.code.as_deref() == Some("ENOENT") => Ok(None), + Err(error) => Err(error), + } +} + +pub fn exists_sync(path: String) -> Result { + Ok(matches!(stat_sync(path, true), Ok(Some(_)))) +} + +pub fn rename_sync(from: String, to: String) -> Result { + fs::rename(&from, &to).map_err(|error| errors::from_io(error, &from))?; + Ok(true) +} + +pub fn copy_file_sync(from: String, to: String) -> Result { + fs::copy(&from, &to).map_err(|error| errors::from_io(error, &from))?; + Ok(true) +} + +/// The one place a write happens, so bytes and text share the flag and mode +/// translation exactly. +fn write_bytes(path: &str, data: &[u8], flags: f64, mode: f64) -> Result<(), ModuleError> { + let mut file = open_options(flags, crate::module::mode(mode)) + .open(path) + .map_err(|error| errors::from_io(error, path))?; + file.write_all(data) + .map_err(|error| errors::from_io(error, path)) +} + +fn stat_of(path: &str, follow_symlinks: bool) -> Result { + let metadata = if follow_symlinks { + fs::metadata(path) + } else { + fs::symlink_metadata(path) + } + .map_err(|error| errors::from_io(error, path))?; + Ok(stat_result(&metadata)) +} + +/// The generated `StatResult`, filled from `std::fs::Metadata`. +/// +/// Node's `Stats` names the Unix inode fields even on Windows, where they have +/// no meaning; zero is what Node itself reports there. +fn stat_result(metadata: &fs::Metadata) -> StatResult { + let file_type = metadata.file_type(); + let kind = if file_type.is_file() { + kind::FILE + } else if file_type.is_dir() { + kind::DIRECTORY + } else if file_type.is_symlink() { + kind::SYMLINK + } else { + kind::OTHER + }; + + #[cfg(unix)] + let (mode, dev, ino, nlink, uid, gid) = { + use std::os::unix::fs::MetadataExt as _; + ( + metadata.mode() as f64, + metadata.dev() as f64, + metadata.ino() as f64, + metadata.nlink() as f64, + metadata.uid() as f64, + metadata.gid() as f64, + ) + }; + #[cfg(not(unix))] + let (mode, dev, ino, nlink, uid, gid) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + // `ctime` is the inode change time on Unix; Windows has no such thing, and + // its creation time is the closest equivalent. + #[cfg(unix)] + let ctime_ms = { + use std::os::unix::fs::MetadataExt as _; + metadata.ctime() as f64 * 1000.0 + metadata.ctime_nsec() as f64 / 1_000_000.0 + }; + #[cfg(not(unix))] + let ctime_ms = metadata + .created() + .map(millis) + .unwrap_or_else(|_| metadata.modified().map(millis).unwrap_or(0.0)); + + // A file system that cannot report creation gives 0, as Node does, rather + // than pretending the modification time is the birth time. + let birthtime_ms = metadata.created().map(millis).unwrap_or(0.0); + + StatResult { + size: metadata.len() as f64, + mode, + kind, + atime_ms: metadata.accessed().map(millis).unwrap_or(0.0), + mtime_ms: metadata.modified().map(millis).unwrap_or(0.0), + ctime_ms, + birthtime_ms, + dev, + ino, + nlink, + uid, + gid, + } +} + +fn copy_path(from: &str, to: &str, recursive: bool) -> Result<(), ModuleError> { + let metadata = fs::symlink_metadata(from).map_err(|error| errors::from_io(error, from))?; + if !metadata.is_dir() { + fs::copy(from, to).map_err(|error| errors::from_io(error, from))?; + return Ok(()); + } + if !recursive { + return Err(errors::invalid( + "EISDIR", + format!("EISDIR: illegal operation on a directory, {from}"), + )); + } + copy_tree(Path::new(from), Path::new(to)) +} + +/// Refuse to copy a directory into itself or a directory beneath it. +/// +/// The destination may not exist yet, so the comparison is against its nearest +/// existing ancestor: `a` into `a/b/c` has to be caught before `a/b/c` exists, +/// which is the whole point. Both sides are canonicalised so a symlinked path +/// cannot slip past the check. +/// +/// Node's code and wording are reproduced rather than invented; `cpSync` reports +/// `ERR_FS_CP_EINVAL` with `Cannot copy /tmp/cpa/ to a subdirectory of self +/// /tmp/cpa/sub`, and callers match on both. +fn refuse_copy_into_self(from: &Path, to: &Path) -> Result<(), ModuleError> { + let resolve = |path: &Path| -> Option { + let mut existing = path.to_path_buf(); + let mut tail = Vec::new(); + loop { + if let Ok(canonical) = existing.canonicalize() { + return Some( + tail.iter() + .rev() + .fold(canonical, |base, part| base.join(part)), + ); + } + let name = existing.file_name().map(|name| name.to_owned())?; + tail.push(name); + if !existing.pop() { + return None; + } + } + }; + let Some(source) = resolve(from) else { + return Ok(()); + }; + let Some(destination) = resolve(to) else { + return Ok(()); + }; + if destination == source || destination.starts_with(&source) { + return Err(errors::invalid( + "ERR_FS_CP_EINVAL", + format!( + "Cannot copy {} to a subdirectory of self {}", + from.display(), + to.display() + ), + )); + } + Ok(()) +} + +fn copy_tree(from: &Path, to: &Path) -> Result<(), ModuleError> { + let target = to.to_string_lossy().into_owned(); + // Checked before anything is created: copying a directory into its own + // subtree would otherwise recurse forever, and the tree it built on the way + // down would have to be cleaned up by the caller. + refuse_copy_into_self(from, to)?; + fs::create_dir_all(to).map_err(|error| errors::from_io(error, &target))?; + let children = fs::read_dir(from).map_err(|error| errors::from_io(error, &source(from)))?; + for child in children { + let child = child.map_err(|error| errors::from_io(error, &source(from)))?; + let path = child.path(); + let destination = to.join(child.file_name()); + // `file_type` does not follow links, which keeps a link cycle in the + // source tree from turning into infinite recursion. + let file_type = child + .file_type() + .map_err(|error| errors::from_io(error, &source(&path)))?; + if file_type.is_dir() { + copy_tree(&path, &destination)?; + } else if file_type.is_symlink() { + copy_symlink(&path, &destination)?; + } else { + fs::copy(&path, &destination) + .map_err(|error| errors::from_io(error, &source(&path)))?; + } + } + Ok(()) +} + +#[cfg(unix)] +fn copy_symlink(from: &Path, to: &Path) -> Result<(), ModuleError> { + let target = fs::read_link(from).map_err(|error| errors::from_io(error, &source(from)))?; + std::os::unix::fs::symlink(&target, to).map_err(|error| errors::from_io(error, &source(from))) +} + +/// Windows needs a privilege to create a symbolic link, so there the link is +/// followed and its target copied, which is what a caller copying a tree wants +/// to end up with anyway. +#[cfg(not(unix))] +fn copy_symlink(from: &Path, to: &Path) -> Result<(), ModuleError> { + fs::copy(from, to) + .map(|_| ()) + .map_err(|error| errors::from_io(error, &source(from))) +} + +fn source(path: &Path) -> String { + path.to_string_lossy().into_owned() +} + +#[cfg(unix)] +fn set_mode(path: &str, mode: f64) -> Result<(), ModuleError> { + use std::os::unix::fs::PermissionsExt as _; + let bits = if mode.is_finite() && mode >= 0.0 { + mode as u32 & 0o7777 + } else { + 0o666 + }; + fs::set_permissions(path, fs::Permissions::from_mode(bits)) + .map_err(|error| errors::from_io(error, path)) +} + +/// Windows has no permission bits. The closest equivalent is the read-only +/// attribute, so a mode without a write bit marks the file read-only and any +/// other mode clears it; nothing else about the mode survives. +#[cfg(not(unix))] +fn set_mode(path: &str, mode: f64) -> Result<(), ModuleError> { + let metadata = fs::metadata(path).map_err(|error| errors::from_io(error, path))?; + let mut permissions = metadata.permissions(); + permissions.set_readonly(mode.is_finite() && mode > 0.0 && mode as u32 & 0o200 == 0); + fs::set_permissions(path, permissions).map_err(|error| errors::from_io(error, path)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::module::flags; + use tempfile::TempDir; + + /// The async wrappers never actually suspend (they call `std::fs` directly), + /// so a single-poll executor is enough and keeps the tests free of a runtime. + fn block_on(future: F) -> F::Output { + let mut future = std::pin::pin!(future); + let waker = std::task::Waker::noop(); + let mut context = std::task::Context::from_waker(waker); + // One poll, no loop: these helpers do their work inline, so a `Pending` + // would mean the helper is not synchronous after all, which the test + // should fail loudly on rather than spin. + match future.as_mut().poll(&mut context) { + std::task::Poll::Ready(value) => value, + std::task::Poll::Pending => panic!("the wrappers are synchronous"), + } + } + + fn tempdir() -> TempDir { + TempDir::new().expect("a temporary directory") + } + + fn text(path: &Path) -> String { + path.to_string_lossy().into_owned() + } + + /// `'w'` in Node's table. + fn write_flags() -> f64 { + f64::from(flags::WRONLY | flags::CREAT | flags::TRUNC) + } + + #[test] + fn text_round_trip() { + let dir = tempdir(); + let path = dir.path().join("note.txt"); + + block_on(write_text_file( + text(&path), + "hello".to_string(), + write_flags(), + 0.0, + )) + .expect("write"); + assert_eq!( + block_on(read_text_file(text(&path))).expect("read"), + "hello" + ); + assert_eq!( + read_text_file_sync(text(&path)).expect("read sync"), + "hello" + ); + + assert!( + write_text_file_sync(text(&path), "again".to_string(), write_flags(), 0.0) + .expect("write sync") + ); + assert_eq!(fs::read_to_string(&path).expect("on disk"), "again"); + } + + #[test] + fn bytes_round_trip() { + let dir = tempdir(); + let path = dir.path().join("bytes.bin"); + let bytes = ArrayBuffer::from(vec![0u8, 1, 254, 255]); + + block_on(write_file(text(&path), bytes.clone(), write_flags(), 0.0)).expect("write"); + assert_eq!( + block_on(read_file(text(&path))).expect("read").as_slice(), + &[0u8, 1, 254, 255] + ); + assert_eq!( + read_file_sync(text(&path)).expect("read sync").into_vec(), + vec![0u8, 1, 254, 255] + ); + } + + #[test] + fn an_append_flag_keeps_the_existing_bytes() { + let dir = tempdir(); + let path = dir.path().join("log.txt"); + fs::write(&path, b"one").expect("seed"); + + block_on(write_text_file( + text(&path), + "two".to_string(), + f64::from(flags::WRONLY | flags::CREAT | flags::APPEND), + 0.0, + )) + .expect("append"); + + assert_eq!(fs::read_to_string(&path).expect("on disk"), "onetwo"); + } + + #[test] + fn stat_reports_kind_and_size() { + let dir = tempdir(); + let file = dir.path().join("data.bin"); + fs::write(&file, b"12345").expect("seed"); + + let stats = block_on(stat(text(&file), true)).expect("stat"); + assert_eq!(stats.kind, kind::FILE); + assert_eq!(stats.size, 5.0); + assert!(stats.mtime_ms > 0.0); + #[cfg(unix)] + assert_ne!( + stats.mode as u32 & 0o777, + 0, + "the mode bits come from MetadataExt" + ); + + let directory = block_on(stat(text(dir.path()), true)).expect("stat dir"); + assert_eq!(directory.kind, kind::DIRECTORY); + } + + #[cfg(unix)] + #[test] + fn stat_follows_links_only_when_asked() { + let dir = tempdir(); + let target = dir.path().join("target.txt"); + fs::write(&target, b"x").expect("seed"); + let link = dir.path().join("link.txt"); + std::os::unix::fs::symlink(&target, &link).expect("symlink"); + + assert_eq!( + block_on(stat(text(&link), false)).expect("lstat").kind, + kind::SYMLINK + ); + assert_eq!( + block_on(stat(text(&link), true)).expect("stat").kind, + kind::FILE + ); + } + + #[test] + fn stat_sync_is_none_for_a_missing_path() { + let dir = tempdir(); + let missing = text(&dir.path().join("absent.txt")); + assert!(stat_sync(missing, true).expect("stat").is_none()); + } + + #[test] + fn exists_sync_is_false_for_a_missing_path() { + let dir = tempdir(); + assert!(!exists_sync(text(&dir.path().join("absent.txt"))).expect("exists")); + assert!(exists_sync(text(dir.path())).expect("exists")); + } + + #[test] + fn a_missing_file_is_enoent() { + let dir = tempdir(); + let missing = text(&dir.path().join("gone.txt")); + + let error = block_on(read_text_file(missing.clone())).expect_err("read"); + assert_eq!(error.code.as_deref(), Some("ENOENT")); + assert!(error + .message + .starts_with("ENOENT: no such file or directory")); + + let error = read_file_sync(missing).expect_err("read sync"); + assert_eq!(error.code.as_deref(), Some("ENOENT")); + } + + #[test] + fn rename_moves_a_file() { + let dir = tempdir(); + let from = dir.path().join("a.txt"); + let to = dir.path().join("b.txt"); + fs::write(&from, b"a").expect("seed"); + + block_on(rename(text(&from), text(&to))).expect("rename"); + assert!(!from.exists()); + assert_eq!(fs::read_to_string(&to).expect("on disk"), "a"); + + assert!(rename_sync(text(&to), text(&from)).expect("rename sync")); + assert!(from.exists()); + } + + #[test] + fn copy_copies_a_file() { + let dir = tempdir(); + let from = dir.path().join("one.txt"); + fs::write(&from, b"copy me").expect("seed"); + let to = dir.path().join("two.txt"); + + block_on(copy(text(&from), text(&to), false)).expect("copy"); + assert_eq!(fs::read_to_string(&to).expect("on disk"), "copy me"); + + let third = dir.path().join("three.txt"); + assert!(copy_file_sync(text(&from), text(&third)).expect("copy sync")); + assert_eq!(fs::read_to_string(&third).expect("on disk"), "copy me"); + } + + #[test] + fn copy_copies_a_directory_tree_when_recursive() { + let dir = tempdir(); + let from = dir.path().join("tree"); + fs::create_dir_all(from.join("nested")).expect("tree"); + fs::write(from.join("top.txt"), b"top").expect("seed"); + fs::write(from.join("nested").join("deep.txt"), b"deep").expect("seed"); + let to = dir.path().join("copy"); + + block_on(copy(text(&from), text(&to), true)).expect("copy tree"); + assert_eq!( + fs::read_to_string(to.join("nested").join("deep.txt")).expect("on disk"), + "deep" + ); + + let error = block_on(copy(text(&from), text(&dir.path().join("nope")), false)) + .expect_err("a directory without recursive"); + assert_eq!(error.code.as_deref(), Some("EISDIR")); + } + + #[test] + fn copying_a_directory_into_itself_is_refused() { + let dir = tempdir(); + let from = dir.path().join("tree"); + fs::create_dir_all(from.join("sub")).expect("tree"); + fs::write(from.join("top.txt"), b"top").expect("seed"); + + // Into a directory beneath itself. This used to recurse until the path + // got too long, and left whatever it had built on the way down. + let into_self = from.join("sub"); + let error = block_on(copy(text(&from), text(&into_self), true)) + .expect_err("copy into a subdirectory of self"); + assert_eq!(error.code.as_deref(), Some("ERR_FS_CP_EINVAL")); + assert!( + error.message.starts_with("Cannot copy"), + "{}", + error.message + ); + // The check runs before anything is created, so the destination is still + // just the empty directory it was. + assert_eq!( + fs::read_dir(&into_self).expect("still there").count(), + 0, + "nothing may be created when the copy is refused" + ); + + // The directory itself is the same case. + let error = block_on(copy(text(&from), text(&from), true)).expect_err("copy onto itself"); + assert_eq!(error.code.as_deref(), Some("ERR_FS_CP_EINVAL")); + } + + #[test] + fn truncate_changes_the_length() { + let dir = tempdir(); + let path = dir.path().join("long.txt"); + fs::write(&path, b"0123456789").expect("seed"); + + block_on(truncate(text(&path), 4.0)).expect("truncate"); + assert_eq!(fs::read_to_string(&path).expect("on disk"), "0123"); + } + + #[test] + fn utimes_sets_both_times() { + let dir = tempdir(); + let path = dir.path().join("stamp.txt"); + fs::write(&path, b"x").expect("seed"); + + block_on(utimes( + text(&path), + 1_000_000_000_000.0, + 1_100_000_000_000.0, + )) + .expect("utimes"); + + let stats = block_on(stat(text(&path), true)).expect("stat"); + assert_eq!(stats.mtime_ms, 1_100_000_000_000.0); + } + + #[cfg(unix)] + #[test] + fn chmod_sets_the_permission_bits() { + use std::os::unix::fs::PermissionsExt as _; + let dir = tempdir(); + let path = dir.path().join("locked.txt"); + fs::write(&path, b"x").expect("seed"); + + block_on(chmod(text(&path), f64::from(0o640u32))).expect("chmod"); + + let mode = fs::metadata(&path).expect("metadata").permissions().mode() & 0o777; + assert_eq!(mode, 0o640); + } +} diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/handles.rs b/vendor/react-native-gpui/modules/fs/desktop/src/handles.rs new file mode 100644 index 0000000000..8bbb44c745 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/handles.rs @@ -0,0 +1,697 @@ +//! Open descriptors: `open`, `read`, `write`, `fstat`, `fsync`, `ftruncate` +//! and `close`. +//! +//! A descriptor is an id from the module's table plus a clone of the file that +//! shares its offset, which is what a descriptor is to the OS. The table owns +//! the original, so dropping the module closes every handle; ids are never +//! reused, so a stale `fd` that JS kept across a `close` fails with `EBADF` +//! instead of reaching an unrelated file. +//! +//! These functions are synchronous even though the trait methods around them are +//! `async`: the module has a thread of its own, so a blocking call here never +//! blocks JS or the UI. + +use std::fs::File; +use std::io::{Read as _, Seek as _, Write as _}; + +use rn_gpui_module::{ArrayBuffer, ModuleError}; + +use crate::errors; +use crate::module::{bad_fd, descriptor, flags, kind, millis, open_options}; +use crate::StatResult; + +pub fn open( + module: &crate::RNFsModule, + path: String, + flags: f64, + mode: f64, +) -> Result { + // The table owns the file; everything the caller asks of it goes through + // `module.file(fd)`, which hands back a clone sharing the same offset. + let file = open_options(flags, crate::module::mode(mode)) + .open(&path) + .map_err(|error| errors::from_io(error, &path))?; + + let id = module.take_fd(); + // `RDONLY` is zero, so "writable" is the presence of any write bit rather + // than the absence of a read bit. + let writable = (flags as i32) & (flags::WRONLY | flags::RDWR) != 0; + module + .files + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .insert(id, (file, writable)); + Ok(f64::from(id)) +} + +pub fn read( + module: &crate::RNFsModule, + fd: f64, + length: f64, + position: f64, +) -> Result { + let id = descriptor(fd)?; + let length = crate::module::length(length)?; + let mut file = module.file(fd)?; + + // Nothing larger than the file can be read, so the buffer is capped at what + // remains rather than at what was asked for. A caller that passes a huge + // `length` — which `readToEnd` used to do deliberately — would otherwise ask + // for that many bytes up front and get whatever the allocator did with it. + let cap = match offset(position)? { + Some(start) => file + .metadata() + .ok() + .and_then(|meta| meta.len().checked_sub(start)) + .map_or(length, |remaining| length.min(remaining as usize)), + None => file + .metadata() + .ok() + .and_then(|meta| { + // A cursor read starts wherever the handle is, so the remainder + // is measured from there rather than from the start. + let current = file.stream_position().ok()?; + meta.len().checked_sub(current) + }) + .map_or(length, |remaining| length.min(remaining as usize)), + }; + let mut buffer = vec![0u8; cap]; + let filled = match offset(position)? { + Some(start) => read_at(&mut file, &mut buffer, start, id)?, + None => read_cursor(&mut file, &mut buffer, id)?, + }; + // A read that stopped at end of file is a short read, not a failure, so the + // buffer is shrunk to what the OS actually produced rather than padded. + buffer.truncate(filled); + Ok(ArrayBuffer::from(buffer)) +} + +pub fn write( + module: &crate::RNFsModule, + fd: f64, + data: ArrayBuffer, + position: f64, +) -> Result { + let id = descriptor(fd)?; + let (mut file, writable) = module.file_and_writability(fd)?; + // Checked from the flags the handle was opened with, not from what the OS + // says: a write to a read-only handle is `EBADF` in Node, `EACCES` on + // Windows and `EBADF` on Unix. + if !writable { + return Err(bad_fd(id)); + } + let bytes = data.as_slice(); + if bytes.is_empty() { + return Ok(0.0); + } + + // Read before the borrow below: `positioned_write` needs `&mut File` so the + // Windows branch can seek, and a match guard borrowing the file would overlap. + let append_only = appends(&file); + let written = match offset(position)? { + // An append-mode descriptor appends whatever offset the caller passes: + // a cursor write does that because the file was opened with `O_APPEND` + // (or `FILE_APPEND_DATA`), and Linux also appends for a positioned + // write. Ignoring the offset keeps the platforms alike and is what + // callers of `appendFile` expect. + Some(start) if !append_only => write_at(&mut file, bytes, start, id)?, + _ => write_cursor(&mut file, bytes, id)?, + }; + Ok(written as f64) +} + +pub fn fstat(module: &crate::RNFsModule, fd: f64) -> Result { + let id = descriptor(fd)?; + let file = module.file(fd)?; + let metadata = file.metadata().map_err(|error| io_error(error, id))?; + Ok(stat_result(&metadata)) +} + +pub fn fsync(module: &crate::RNFsModule, fd: f64) -> Result<(), ModuleError> { + let id = descriptor(fd)?; + let file = module.file(fd)?; + file.sync_all().map_err(|error| io_error(error, id)) +} + +pub fn ftruncate(module: &crate::RNFsModule, fd: f64, length: f64) -> Result<(), ModuleError> { + let id = descriptor(fd)?; + let length = crate::module::length(length)?; + let file = module.file(fd)?; + file.set_len(length as u64) + .map_err(|error| io_error(error, id)) +} + +pub fn close(module: &crate::RNFsModule, fd: f64) -> Result<(), ModuleError> { + let id = descriptor(fd)?; + let mut files = module + .files + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + // Removing rather than merely dropping the entry is what makes a second + // close report `EBADF`: the id is gone, and it is never handed out again. + files.remove(&id).ok_or_else(|| bad_fd(id))?; + Ok(()) +} + +/// A failure from an operation on a descriptor. +/// +/// The table may still know an id the kernel has already rejected — a write to a +/// read-only descriptor is the usual way — and Node reports that as `EBADF`, not +/// as a generic I/O error. Everything else goes through the shared mapping, with +/// the descriptor standing in for the path a Node message names. +fn io_error(error: std::io::Error, id: u32) -> ModuleError { + #[cfg(unix)] + if error.raw_os_error() == Some(libc::EBADF) { + return bad_fd(id); + } + errors::from_io(error, &id.to_string()) +} + +/// The byte offset a `position` argument names, or `None` for "the current +/// offset", which is what a negative position means to `node:fs`. +fn offset(position: f64) -> Result, ModuleError> { + if !position.is_finite() { + return Err(errors::invalid( + "EINVAL", + format!("EINVAL: invalid argument, position {position}"), + )); + } + if position < 0.0 { + return Ok(None); + } + Ok(Some(position as u64)) +} + +/// Whether the descriptor was opened for append. +/// +/// The table stores only the `File`, so the bit is read back from the descriptor +/// itself with `F_GETFL`: that reports the flag the kernel actually holds, which +/// is more reliable than tracking a second copy beside the table. +#[cfg(unix)] +fn appends(file: &File) -> bool { + use std::os::unix::io::AsRawFd as _; + // SAFETY: `file` owns a valid descriptor for the duration of the call; `F_GETFL` takes no argument. + let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFL) }; + flags >= 0 && flags & libc::O_APPEND != 0 +} + +/// Windows keeps the append bit inside the handle's granted access, with no +/// portable way to read it back, so the answer is "no" here. Cursor writes still +/// append — `append(true)` opens with `FILE_APPEND_DATA`, which does that in the +/// kernel — and a write with an explicit offset lands at that offset, which is +/// what a caller using `pwrite` on Windows would expect. +#[cfg(not(unix))] +fn appends(_file: &File) -> bool { + false +} + +fn read_at(file: &mut File, buffer: &mut [u8], offset: u64, id: u32) -> Result { + let mut filled = 0; + while filled < buffer.len() { + match positioned_read(file, &mut buffer[filled..], offset + filled as u64) { + Ok(0) => break, + Ok(count) => filled += count, + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, + Err(error) => return Err(io_error(error, id)), + } + } + Ok(filled) +} + +fn read_cursor(file: &mut File, buffer: &mut [u8], id: u32) -> Result { + let mut filled = 0; + while filled < buffer.len() { + match file.read(&mut buffer[filled..]) { + Ok(0) => break, + Ok(count) => filled += count, + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, + Err(error) => return Err(io_error(error, id)), + } + } + Ok(filled) +} + +fn write_at(file: &mut File, bytes: &[u8], offset: u64, id: u32) -> Result { + let mut written = 0; + while written < bytes.len() { + match positioned_write(file, &bytes[written..], offset + written as u64) { + // A regular file does not write zero bytes for a non-empty buffer; + // treating it as `EIO` keeps the loop from spinning forever. + Ok(0) => return Err(errors::invalid("EIO", format!("EIO: i/o error, {id}"))), + Ok(count) => written += count, + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, + Err(error) => return Err(io_error(error, id)), + } + } + Ok(written) +} + +fn write_cursor(file: &mut File, bytes: &[u8], id: u32) -> Result { + file.write_all(bytes).map_err(|error| io_error(error, id))?; + Ok(bytes.len()) +} + +#[cfg(unix)] +fn positioned_read(file: &mut File, buffer: &mut [u8], offset: u64) -> std::io::Result { + use std::os::unix::fs::FileExt as _; + // `read_at` takes `&self`, but the call sites hold `&mut File` so the + // Windows branch — which has to seek — shares one signature. + (&*file).read_at(buffer, offset) +} + +/// Windows' positional read, with the cursor left where it was. +/// +/// `FileExt::seek_read` is documented as not affecting the cursor, but on a +/// synchronous handle — which is what `File::open` gives — the underlying +/// `ReadFile` with an `OVERLAPPED` offset keeps the file position as a side +/// effect. Node's positional forms leave the descriptor alone, and so does the +/// Unix branch above, so the position is saved and restored here. Without this a +/// positioned read moved the cursor, and a following cursor read started from the +/// wrong place. +#[cfg(windows)] +fn positioned_read(file: &mut File, buffer: &mut [u8], offset: u64) -> std::io::Result { + use std::io::Seek as _; + use std::os::windows::fs::FileExt as _; + let saved = file.stream_position(); + let result = file.seek_read(buffer, offset); + if let Ok(position) = saved { + let _ = file.seek(std::io::SeekFrom::Start(position)); + } + result +} + +#[cfg(unix)] +fn positioned_write(file: &mut File, bytes: &[u8], offset: u64) -> std::io::Result { + use std::os::unix::fs::FileExt as _; + (&*file).write_at(bytes, offset) +} + +/// Windows' positional write, with the cursor left where it was. Same reason as +/// `positioned_read` above. +#[cfg(windows)] +fn positioned_write(file: &mut File, bytes: &[u8], offset: u64) -> std::io::Result { + use std::io::Seek as _; + use std::os::windows::fs::FileExt as _; + let saved = file.stream_position(); + let result = file.seek_write(bytes, offset); + if let Ok(position) = saved { + let _ = file.seek(std::io::SeekFrom::Start(position)); + } + result +} + +/// The generated `StatResult`, filled from `std::fs::Metadata`. +/// +/// `files::stat` builds the same shape for a path. The two are deliberately +/// separate: `files` and `handles` are independent, and twenty duplicated lines +/// cost less than one file reaching into the other's internals. +fn stat_result(metadata: &std::fs::Metadata) -> StatResult { + let file_type = metadata.file_type(); + let kind = if file_type.is_file() { + kind::FILE + } else if file_type.is_dir() { + kind::DIRECTORY + } else if file_type.is_symlink() { + kind::SYMLINK + } else { + kind::OTHER + }; + + // Node's `Stats` names the Unix inode fields even on Windows, where they + // have no meaning; zero is what Node reports there too. + #[cfg(unix)] + let (mode, dev, ino, nlink, uid, gid) = { + use std::os::unix::fs::MetadataExt as _; + ( + metadata.mode() as f64, + metadata.dev() as f64, + metadata.ino() as f64, + metadata.nlink() as f64, + metadata.uid() as f64, + metadata.gid() as f64, + ) + }; + #[cfg(not(unix))] + let (mode, dev, ino, nlink, uid, gid) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + + // `ctime` is the inode change time on Unix; Windows has no equivalent, and + // its creation time is the closest thing it has. + #[cfg(unix)] + let ctime_ms = { + use std::os::unix::fs::MetadataExt as _; + metadata.ctime() as f64 * 1000.0 + metadata.ctime_nsec() as f64 / 1_000_000.0 + }; + #[cfg(not(unix))] + let ctime_ms = metadata + .created() + .map(millis) + .unwrap_or_else(|_| metadata.modified().map(millis).unwrap_or(0.0)); + + // A file system that cannot report creation gives 0, as Node does, rather + // than pretending the modification time is the birth time. + StatResult { + size: metadata.len() as f64, + mode, + kind, + atime_ms: metadata.accessed().map(millis).unwrap_or(0.0), + mtime_ms: metadata.modified().map(millis).unwrap_or(0.0), + ctime_ms, + birthtime_ms: metadata.created().map(millis).unwrap_or(0.0), + dev, + ino, + nlink, + uid, + gid, + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + use std::sync::Arc; + + use rn_gpui_module::{EventEmitter, ModuleInstance, Value}; + use tempfile::TempDir; + + use super::*; + use crate::native_fs_module; + + /// The async trait methods never actually suspend — they call these helpers + /// directly — so a single-poll executor drives the generated glue without a + /// runtime. + fn block_on(future: F) -> F::Output { + let mut future = std::pin::pin!(future); + let waker = std::task::Waker::noop(); + let mut context = std::task::Context::from_waker(waker); + loop { + match future.as_mut().poll(&mut context) { + std::task::Poll::Ready(value) => return value, + std::task::Poll::Pending => std::thread::yield_now(), + } + } + } + + /// `NativeFsEvents` has a private emitter and no constructor, so a test + /// cannot call `RNFsModule::create` itself. Building the instance through the + /// descriptor the bridge uses is the supported way in, and it exercises the + /// generated glue as a bonus. + fn module() -> Arc { + (native_fs_module::().create)(EventEmitter::new(1)) + } + + fn index(name: &str) -> usize { + native_fs_module::() + .methods + .iter() + .position(|method| method.name == name) + .unwrap_or_else(|| panic!("the spec has no method {name}")) + } + + fn call( + module: &Arc, + name: &str, + args: Vec, + ) -> Result { + block_on(module.clone().call_promise(index(name), args)?) + } + + fn text(path: &Path) -> String { + path.to_string_lossy().into_owned() + } + + fn open_fd(module: &Arc, path: &Path, flags_value: i32) -> f64 { + let value = call( + module, + "open", + vec![ + Value::String(text(path)), + Value::Number(f64::from(flags_value)), + Value::Number(0.0), + ], + ) + .expect("open"); + match value { + Value::Number(fd) => fd, + other => panic!("expected a descriptor, got {}", other.type_name()), + } + } + + fn read_bytes( + module: &Arc, + fd: f64, + length: f64, + position: f64, + ) -> Result, ModuleError> { + match call( + module, + "read", + vec![ + Value::Number(fd), + Value::Number(length), + Value::Number(position), + ], + )? { + Value::Buffer(bytes) => Ok(bytes), + other => panic!("expected bytes, got {}", other.type_name()), + } + } + + fn write_bytes( + module: &Arc, + fd: f64, + bytes: &[u8], + position: f64, + ) -> Result { + match call( + module, + "write", + vec![ + Value::Number(fd), + Value::Buffer(bytes.to_vec()), + Value::Number(position), + ], + )? { + Value::Number(written) => Ok(written), + other => panic!("expected a count, got {}", other.type_name()), + } + } + + fn stat_size(module: &Arc, fd: f64) -> f64 { + let value = call(module, "fstat", vec![Value::Number(fd)]).expect("fstat"); + let Value::Object(fields) = value else { + panic!("expected a stat object"); + }; + fields + .iter() + .find(|(name, _)| name == "size") + .and_then(|(_, value)| match value { + Value::Number(size) => Some(*size), + _ => None, + }) + .expect("a size field") + } + + fn close_fd(module: &Arc, fd: f64) -> Result { + call(module, "close", vec![Value::Number(fd)]) + } + + /// Open a fresh file containing `contents` and hand back its descriptor. + fn opened(module: &Arc, dir: &TempDir, contents: &str) -> (String, f64) { + let path = dir.path().join("data.txt"); + std::fs::write(&path, contents).expect("write the fixture file"); + let fd = open_fd(module, &path, flags::RDWR); + (text(&path), fd) + } + + #[test] + fn a_handle_reads_back_what_it_wrote() { + let dir = TempDir::new().expect("a temporary directory"); + let path = dir.path().join("note.txt"); + let module = module(); + + let fd = open_fd(&module, &path, flags::WRONLY | flags::CREAT | flags::TRUNC); + assert_eq!( + write_bytes(&module, fd, b"hello", -1.0).expect("write"), + 5.0 + ); + assert_eq!(stat_size(&module, fd), 5.0); + close_fd(&module, fd).expect("close"); + + let fd = open_fd(&module, &path, flags::RDONLY); + assert_eq!(read_bytes(&module, fd, 5.0, -1.0).expect("read"), b"hello"); + close_fd(&module, fd).expect("close"); + } + + #[test] + fn a_positioned_read_leaves_the_cursor_alone() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let (_, fd) = opened(&module, &dir, "abcdef"); + + assert_eq!(read_bytes(&module, fd, 2.0, 0.0).expect("read"), b"ab"); + // The cursor never moved, so a cursor read starts at the beginning. + assert_eq!(read_bytes(&module, fd, 2.0, -1.0).expect("read"), b"ab"); + assert_eq!(read_bytes(&module, fd, 2.0, -1.0).expect("read"), b"cd"); + close_fd(&module, fd).expect("close"); + } + + #[test] + fn a_positioned_write_only_touches_its_range() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let (path, fd) = opened(&module, &dir, "abcdef"); + + assert_eq!(write_bytes(&module, fd, b"XY", 2.0).expect("write"), 2.0); + assert_eq!(stat_size(&module, fd), 6.0); + assert_eq!(std::fs::read(&path).expect("on disk"), b"abXYef"); + close_fd(&module, fd).expect("close"); + } + + #[test] + fn reading_past_the_end_is_a_short_read_not_an_error() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let (_, fd) = opened(&module, &dir, "abc"); + + // Two bytes asked for, one left: the buffer holds exactly one byte. + assert_eq!(read_bytes(&module, fd, 2.0, 2.0).expect("read"), b"c"); + // Nothing left at all: an empty buffer, still a success. + assert_eq!(read_bytes(&module, fd, 8.0, 10.0).expect("read"), b""); + assert_eq!(read_bytes(&module, fd, 8.0, -1.0).expect("read"), b"abc"); + assert_eq!(read_bytes(&module, fd, 8.0, -1.0).expect("read"), b""); + close_fd(&module, fd).expect("close"); + } + + #[test] + fn a_huge_length_only_allocates_what_the_file_holds() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let (_, fd) = opened(&module, &dir, "0123456789"); + + // The buffer is capped at what remains, so this returns rather than + // trying to allocate four gigabytes. Finishing at all is the check: an + // allocation of `u32::MAX` bytes would abort the test process instead. + let bytes = read_bytes(&module, fd, u32::MAX as f64, 0.0).expect("read"); + assert_eq!(bytes, b"0123456789"); + close_fd(&module, fd).expect("close"); + } + + #[test] + fn appending_ignores_the_position() { + let dir = TempDir::new().expect("a temporary directory"); + let path = dir.path().join("log.txt"); + std::fs::write(&path, b"ab").expect("write the fixture file"); + let module = module(); + + let fd = open_fd(&module, &path, flags::APPEND | flags::WRONLY | flags::CREAT); + write_bytes(&module, fd, b"cd", -1.0).expect("append"); + // An append-mode descriptor appends even when an offset is passed, which + // is what the kernel does on Linux and what `appendFile` means. + write_bytes(&module, fd, b"ef", 0.0).expect("append"); + assert_eq!(stat_size(&module, fd), 6.0); + close_fd(&module, fd).expect("close"); + assert_eq!(std::fs::read(&path).expect("on disk"), b"abcdef"); + } + + #[test] + fn truncating_by_descriptor_changes_the_size() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let (_, fd) = opened(&module, &dir, "abcdef"); + + call( + &module, + "ftruncate", + vec![Value::Number(fd), Value::Number(3.0)], + ) + .expect("ftruncate"); + assert_eq!(stat_size(&module, fd), 3.0); + assert_eq!(read_bytes(&module, fd, 16.0, 0.0).expect("read"), b"abc"); + close_fd(&module, fd).expect("close"); + } + + #[test] + fn syncing_a_handle_succeeds() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let (_, fd) = opened(&module, &dir, "abc"); + write_bytes(&module, fd, b"d", 3.0).expect("write"); + call(&module, "fsync", vec![Value::Number(fd)]).expect("fsync"); + close_fd(&module, fd).expect("close"); + } + + #[test] + fn a_second_close_is_ebadf() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let (_, fd) = opened(&module, &dir, "abc"); + + close_fd(&module, fd).expect("the first close"); + let error = close_fd(&module, fd).expect_err("the second close"); + assert_eq!(error.code.as_deref(), Some("EBADF")); + } + + #[test] + fn a_closed_descriptor_is_ebadf_for_every_op() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let (_, fd) = opened(&module, &dir, "abc"); + close_fd(&module, fd).expect("close"); + + let error = read_bytes(&module, fd, 1.0, -1.0).expect_err("read"); + assert_eq!(error.code.as_deref(), Some("EBADF")); + let error = write_bytes(&module, fd, b"x", -1.0).expect_err("write"); + assert_eq!(error.code.as_deref(), Some("EBADF")); + let error = call(&module, "fstat", vec![Value::Number(fd)]).expect_err("fstat"); + assert_eq!(error.code.as_deref(), Some("EBADF")); + } + + #[test] + fn opening_a_missing_file_is_enoent() { + let dir = TempDir::new().expect("a temporary directory"); + let path = dir.path().join("gone.txt"); + let module = module(); + + let error = call( + &module, + "open", + vec![ + Value::String(text(&path)), + Value::Number(f64::from(flags::RDONLY)), + Value::Number(0.0), + ], + ) + .expect_err("open"); + assert_eq!(error.code.as_deref(), Some("ENOENT")); + } + + /// A descriptor the table still knows but the kernel rejects: Node says + /// `EBADF` here, and so must this module, rather than a generic `EIO`. + #[test] + fn writing_through_a_read_only_descriptor_is_ebadf() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let path = dir.path().join("data.txt"); + std::fs::write(&path, b"abc").expect("write the fixture file"); + let fd = open_fd(&module, &path, flags::RDONLY); + + let error = write_bytes(&module, fd, b"x", -1.0).expect_err("write"); + assert_eq!(error.code.as_deref(), Some("EBADF")); + // Reading is still fine, and the file is untouched. + assert_eq!(read_bytes(&module, fd, 3.0, 0.0).expect("read"), b"abc"); + close_fd(&module, fd).expect("close"); + } + + #[test] + fn a_non_finite_position_is_einval() { + let dir = TempDir::new().expect("a temporary directory"); + let module = module(); + let (_, fd) = opened(&module, &dir, "abc"); + + let error = read_bytes(&module, fd, 1.0, f64::NAN).expect_err("read"); + assert_eq!(error.code.as_deref(), Some("EINVAL")); + let error = write_bytes(&module, fd, b"x", f64::INFINITY).expect_err("write"); + assert_eq!(error.code.as_deref(), Some("EINVAL")); + close_fd(&module, fd).expect("close"); + } +} diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/lib.rs b/vendor/react-native-gpui/modules/fs/desktop/src/lib.rs new file mode 100644 index 0000000000..914954eeed --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/lib.rs @@ -0,0 +1,258 @@ +//! `@react-native-gpui/fs`: a Node-shaped file API for the desktop app. +//! +//! The TypeScript spec in `../src/NativeFs.ts` is the contract; the generator +//! turns it into the `NativeFsSpec` trait implemented here. That trait is the +//! only thing the C++ bridge knows about, so the API surface is edited there and +//! regenerated, never in this file. +//! +//! Rust needs a trait's methods in a single `impl` block, so this file holds that +//! block and hands each method to a helper in the file that owns the concern: +//! +//! - `files` — reading, writing, copying, truncating, permissions and times +//! - `dirs` — listing, creating, removing and `mkdtemp` +//! - `handles` — open descriptors, positioned reads and writes +//! - `links` — symbolic links, `realpath` and `access` +//! - `paths` — the well-known folders, created on first read +//! - `dialogs` — the pickers, `reveal` and `openExternal` +//! - `watching` — the watcher behind `watch` and `unwatch` +//! +//! Keeping the bodies in those files is what makes the crate readable; keeping +//! the `impl` here is what makes it compile. + +rn_gpui_module::include_specs!(); + +mod dialogs; +mod dirs; +mod errors; +mod files; +mod handles; +mod links; +mod module; +mod paths; +mod watching; + +pub use module::RNFsModule; + +use rn_gpui_module::{ArrayBuffer, ModuleError}; + +impl NativeFsSpec for RNFsModule { + fn new(events: NativeFsEvents) -> Self { + Self::create(events) + } + + async fn read_file(&self, path: String) -> Result { + files::read_file(path).await + } + + async fn read_text_file(&self, path: String) -> Result { + files::read_text_file(path).await + } + + async fn write_file( + &self, + path: String, + data: ArrayBuffer, + flags: f64, + mode: f64, + ) -> Result<(), ModuleError> { + files::write_file(path, data, flags, mode).await + } + + async fn write_text_file( + &self, + path: String, + data: String, + flags: f64, + mode: f64, + ) -> Result<(), ModuleError> { + files::write_text_file(path, data, flags, mode).await + } + + async fn readdir(&self, path: String, recursive: bool) -> Result, ModuleError> { + dirs::readdir(path, recursive).await + } + + async fn stat(&self, path: String, follow_symlinks: bool) -> Result { + files::stat(path, follow_symlinks).await + } + + async fn mkdir(&self, path: String, recursive: bool, mode: f64) -> Result<(), ModuleError> { + dirs::mkdir(path, recursive, mode).await + } + + async fn rm(&self, path: String, recursive: bool, force: bool) -> Result<(), ModuleError> { + dirs::rm(path, recursive, force).await + } + + async fn rename(&self, from: String, to: String) -> Result<(), ModuleError> { + files::rename(from, to).await + } + + async fn copy(&self, from: String, to: String, recursive: bool) -> Result<(), ModuleError> { + files::copy(from, to, recursive).await + } + + async fn access(&self, path: String, mode: f64) -> Result<(), ModuleError> { + links::access(path, mode) + } + + async fn realpath(&self, path: String) -> Result { + links::realpath(path) + } + + async fn readlink(&self, path: String) -> Result { + links::readlink(path) + } + + async fn symlink( + &self, + target: String, + path: String, + kind: Option, + ) -> Result<(), ModuleError> { + links::symlink(target, path, kind.as_deref()) + } + + async fn truncate(&self, path: String, length: f64) -> Result<(), ModuleError> { + files::truncate(path, length).await + } + + async fn utimes(&self, path: String, atime_ms: f64, mtime_ms: f64) -> Result<(), ModuleError> { + files::utimes(path, atime_ms, mtime_ms).await + } + + async fn chmod(&self, path: String, mode: f64) -> Result<(), ModuleError> { + files::chmod(path, mode).await + } + + async fn mkdtemp(&self, prefix: String) -> Result { + dirs::mkdtemp(prefix).await + } + + async fn open(&self, path: String, flags: f64, mode: f64) -> Result { + handles::open(self, path, flags, mode) + } + + async fn read(&self, fd: f64, length: f64, position: f64) -> Result { + handles::read(self, fd, length, position) + } + + async fn write(&self, fd: f64, data: ArrayBuffer, position: f64) -> Result { + handles::write(self, fd, data, position) + } + + async fn fstat(&self, fd: f64) -> Result { + handles::fstat(self, fd) + } + + async fn fsync(&self, fd: f64) -> Result<(), ModuleError> { + handles::fsync(self, fd) + } + + async fn ftruncate(&self, fd: f64, length: f64) -> Result<(), ModuleError> { + handles::ftruncate(self, fd, length) + } + + async fn close(&self, fd: f64) -> Result<(), ModuleError> { + handles::close(self, fd) + } + + async fn show_open_dialog( + &self, + options: OpenDialogOptions, + ) -> Result, ModuleError> { + dialogs::show_open(options) + } + + async fn show_save_dialog( + &self, + options: SaveDialogOptions, + ) -> Result, ModuleError> { + dialogs::show_save(options) + } + + async fn reveal(&self, path: String) -> Result<(), ModuleError> { + dialogs::reveal(path) + } + + async fn open_external(&self, path: String) -> Result<(), ModuleError> { + dialogs::open_external(path) + } + + async fn watch(&self, path: String, recursive: bool) -> Result { + watching::watch(self, path, recursive) + } + + fn unwatch(&self, id: f64) { + watching::unwatch(self, id); + } + + fn read_file_sync(&self, path: String) -> Result { + files::read_file_sync(path) + } + + fn read_text_file_sync(&self, path: String) -> Result { + files::read_text_file_sync(path) + } + + fn write_file_sync( + &self, + path: String, + data: ArrayBuffer, + flags: f64, + mode: f64, + ) -> Result { + files::write_file_sync(path, data, flags, mode) + } + + fn write_text_file_sync( + &self, + path: String, + data: String, + flags: f64, + mode: f64, + ) -> Result { + files::write_text_file_sync(path, data, flags, mode) + } + + fn stat_sync( + &self, + path: String, + follow_symlinks: bool, + ) -> Result, ModuleError> { + files::stat_sync(path, follow_symlinks) + } + + fn exists_sync(&self, path: String) -> Result { + files::exists_sync(path) + } + + fn readdir_sync(&self, path: String, recursive: bool) -> Result, ModuleError> { + dirs::readdir_sync(path, recursive) + } + + fn mkdir_sync(&self, path: String, recursive: bool, mode: f64) -> Result { + dirs::mkdir_sync(path, recursive, mode) + } + + fn rm_sync(&self, path: String, recursive: bool, force: bool) -> Result { + dirs::rm_sync(path, recursive, force) + } + + fn rename_sync(&self, from: String, to: String) -> Result { + files::rename_sync(from, to) + } + + fn copy_file_sync(&self, from: String, to: String) -> Result { + files::copy_file_sync(from, to) + } + + fn get_paths(&self) -> Result { + paths::known_paths() + } +} + +/// Called by the app's autolink shim, which keeps these modules linked in. +pub fn rn_gpui_register() { + rn_gpui_module::register(native_fs_module::()); +} diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/links.rs b/vendor/react-native-gpui/modules/fs/desktop/src/links.rs new file mode 100644 index 0000000000..cd49bc4ddf --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/links.rs @@ -0,0 +1,336 @@ +//! Links and path checks: `access`, `realpath`, `readlink` and `symlink`. +//! +//! These are the calls whose exact meaning differs most between the platforms, +//! so each one is written against the platform's own primitive rather than +//! approximated from `std::fs` where that would change the answer. + +use std::path::Path; +use std::path::PathBuf; + +use rn_gpui_module::ModuleError; + +use crate::errors; +use crate::module::access; + +pub fn access(path: String, mode: f64) -> Result<(), ModuleError> { + let mode = access_mode(mode)?; + platform_access(&path, mode) +} + +pub fn realpath(path: String) -> Result { + let resolved = dunce::canonicalize(&path).map_err(|error| errors::from_io(error, &path))?; + // A `String` crosses the bridge as UTF-8, so a path that is not valid UTF-8 + // has no faithful representation. Lossy-converting would hand JS a path it + // cannot open, which is worse than failing: `EINVAL` says the argument could + // not be represented rather than that the path does not exist. + resolved + .into_os_string() + .into_string() + .map_err(|unrepresentable| { + errors::invalid( + "EINVAL", + format!( + "EINVAL: invalid argument, path is not valid UTF-8, {}", + Path::new(&unrepresentable).display() + ), + ) + }) +} + +pub fn readlink(path: String) -> Result { + let target = std::fs::read_link(&path).map_err(|error| errors::from_io(error, &path))?; + // As with `realpath`: the target is returned exactly as the file system + // stores it, and a target that is not UTF-8 cannot be a `String`. + target + .into_os_string() + .into_string() + .map_err(|unrepresentable| { + errors::invalid( + "EINVAL", + format!( + "EINVAL: invalid argument, link target is not valid UTF-8, {}", + Path::new(&unrepresentable).display() + ), + ) + }) +} + +pub fn symlink(target: String, path: String, kind: Option<&str>) -> Result<(), ModuleError> { + platform_symlink(&target, &path, kind) +} + +/// Validate the bitmask Node numbers, returning it unchanged. +/// +/// The caller gets Node's own bits back, not `access(2)`'s: `libc` 0.2 defines +/// `R_OK`, `W_OK` and `X_OK` only for Unix, so a Windows build that named them +/// failed with "cannot find value `R_OK` in crate `libc`". The translation to +/// `access(2)` happens in `platform_access`, which is already Unix-only, and the +/// Windows path reads the same Node bits directly. +/// +/// `F_OK` is 0: the mode is not a flag set, and asking for nothing means "does +/// it exist". +fn access_mode(mode: f64) -> Result { + if !mode.is_finite() || mode < 0.0 || mode.fract() != 0.0 { + return Err(errors::invalid( + "EINVAL", + format!("EINVAL: invalid argument, mode {mode}"), + )); + } + let requested = mode as i32; + let known = access::F_OK | access::R_OK | access::W_OK | access::X_OK; + if requested & !known != 0 { + return Err(errors::invalid( + "EINVAL", + format!("EINVAL: invalid argument, mode {requested}"), + )); + } + Ok(requested) +} + +/// `access(2)` is the only way to get Node's semantics — it asks the kernel about +/// *this* process's real uid and gid, including supplementary groups — so the +/// Unix implementation calls it rather than inspecting the mode bits, which +/// would give a different answer for a file the caller does not own. +#[cfg(unix)] +fn platform_access(path: &str, requested: i32) -> Result<(), ModuleError> { + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt as _; + + // `access(2)` uses the same values Node does for these three, but the + // translation is spelled out so a platform whose constants differ still asks + // the right question. This is the only place the `libc` constants are named. + let mut mask = libc::F_OK; + if requested & access::R_OK != 0 { + mask |= libc::R_OK; + } + if requested & access::W_OK != 0 { + mask |= libc::W_OK; + } + if requested & access::X_OK != 0 { + mask |= libc::X_OK; + } + + // A path with an interior NUL cannot be passed to the C API at all; that is + // an invalid argument rather than a file-system failure. + let name = CString::new(std::ffi::OsStr::new(path).as_bytes()).map_err(|_| { + errors::invalid( + "EINVAL", + format!("EINVAL: invalid argument, path contains a null byte, {path}"), + ) + })?; + // `access` reports failure by return value and sets `errno`, so the error is + // built from `last_os_error` and then mapped like any other. + // SAFETY: `name` is a NUL-terminated string that outlives the call. + let result = unsafe { libc::access(name.as_ptr(), mask) }; + if result == 0 { + Ok(()) + } else { + Err(errors::from_io(std::io::Error::last_os_error(), path)) + } +} + +/// Windows has no `access(2)`, so the checks are approximated from what the +/// platform does offer: `F_OK` and `R_OK` are a `metadata()` call (the read bit +/// is not expressible, and a file the process can stat is one it can normally +/// read), `W_OK` tries to open the file for writing, and `X_OK` uses the +/// executable extensions the shell recognises, because Windows has no execute +/// bit to test. +#[cfg(not(unix))] +fn platform_access(path: &str, requested: i32) -> Result<(), ModuleError> { + // `F_OK` asks only that the path resolves; `metadata` follows links, which + // matches `access`'s "does the name lead to something". + std::fs::metadata(path).map_err(|error| errors::from_io(error, path))?; + if requested & access::W_OK != 0 { + std::fs::OpenOptions::new() + .write(true) + .open(path) + .map_err(|error| errors::from_io(error, path))?; + } + if requested & access::X_OK != 0 && !is_executable(Path::new(path)) { + return Err(errors::invalid( + "EACCES", + format!("EACCES: permission denied, {path}"), + )); + } + Ok(()) +} + +/// The extensions `PATHEXT` names, which is how Windows decides a file is a +/// program. A directory is executable for `access`'s purposes because the caller +/// can `chdir` into it. +#[cfg(not(unix))] +fn is_executable(path: &Path) -> bool { + if path.is_dir() { + return true; + } + let extensions = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string()); + let Some(extension) = path.extension().and_then(|value| value.to_str()) else { + return false; + }; + let extension = format!(".{}", extension.to_ascii_uppercase()); + extensions + .split(';') + .any(|candidate| candidate.eq_ignore_ascii_case(&extension)) +} + +/// Unix ignores the link kind: `symlink(2)` does not have one, and the target is +/// resolved lazily on every traversal, so a link to a directory works without +/// being declared as one. +#[cfg(unix)] +fn platform_symlink(target: &str, path: &str, kind: Option<&str>) -> Result<(), ModuleError> { + let _ = kind; + std::os::unix::fs::symlink(target, path).map_err(|error| errors::from_io(error, path)) +} + +/// Windows needs a different call for a file link and a directory link, and it +/// needs the developer-mode privilege (or SeCreateSymbolicLinkPrivilege) for +/// either. The design says to report the OS error rather than fall back to +/// copying, so a caller without the privilege sees the real reason. +/// +/// The kind decides which call, following Node's rules. It cannot be inferred +/// from the target alone: a link may name a path that does not exist yet, and +/// `metadata` would fail on exactly the case that matters. `'junction'` gets +/// `symlink_dir` — a directory symlink rather than a true NTFS junction, which +/// needs a different API and an absolute target; both are traversable, which is +/// what a caller wants. +#[cfg(not(unix))] +fn platform_symlink(target: &str, path: &str, kind: Option<&str>) -> Result<(), ModuleError> { + let directory = match kind { + Some("dir") | Some("junction") => true, + Some("file") => false, + _ => { + // Node's rule when no type is given: resolve the target relative to + // the link's parent — the shell does the same — and take the answer + // from what is there. + let resolved = match Path::new(path).parent() { + Some(parent) => parent.join(target), + None => PathBuf::from(target), + }; + resolved.is_dir() + } + }; + let result = if directory { + std::os::windows::fs::symlink_dir(target, path) + } else { + std::os::windows::fs::symlink_file(target, path) + }; + result.map_err(|error| errors::from_io(error, path)) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn tempdir() -> TempDir { + TempDir::new().expect("a temporary directory") + } + + fn text(path: &Path) -> String { + path.to_string_lossy().into_owned() + } + + #[test] + fn access_finds_a_present_path_and_reports_a_missing_one() { + let dir = tempdir(); + let present = dir.path().join("here.txt"); + std::fs::write(&present, b"x").expect("write the fixture file"); + let absent = dir.path().join("gone.txt"); + + access(text(&present), f64::from(access::F_OK)).expect("a present path exists"); + access(text(&present), f64::from(access::R_OK | access::W_OK)) + .expect("a file we just wrote is readable and writable"); + + let error = access(text(&absent), f64::from(access::F_OK)).expect_err("a missing path"); + assert_eq!(error.code.as_deref(), Some("ENOENT")); + } + + #[test] + fn access_rejects_a_mode_node_does_not_define() { + let dir = tempdir(); + let error = access(text(dir.path()), 8.0).expect_err("an unknown bit"); + assert_eq!(error.code.as_deref(), Some("EINVAL")); + } + + /// `dunce::canonicalize` resolves the whole chain, so the result is the real + /// file rather than the link. Windows is skipped: creating a symlink there + /// needs developer mode, which a CI runner may not have. + #[cfg(unix)] + #[test] + fn realpath_resolves_a_symlink() { + let dir = tempdir(); + let file = dir.path().join("real.txt"); + std::fs::write(&file, b"x").expect("write the fixture file"); + let link = dir.path().join("link.txt"); + std::os::unix::fs::symlink(&file, &link).expect("create the link"); + + assert_eq!( + realpath(text(&link)).expect("realpath"), + realpath(text(&file)).expect("realpath") + ); + assert_eq!( + realpath(text(&file)).expect("realpath"), + dunce::canonicalize(&file) + .expect("canonical") + .to_string_lossy() + ); + } + + #[test] + fn realpath_of_a_missing_path_is_enoent() { + let dir = tempdir(); + let error = realpath(text(&dir.path().join("gone"))).expect_err("missing"); + assert_eq!(error.code.as_deref(), Some("ENOENT")); + } + + /// The link is read, not followed: the target comes back as it was stored, + /// relative or absolute. + #[cfg(unix)] + #[test] + fn readlink_returns_the_stored_target() { + let dir = tempdir(); + let link = dir.path().join("link.txt"); + std::os::unix::fs::symlink("some/relative/target", &link).expect("create the link"); + assert_eq!( + readlink(text(&link)).expect("readlink"), + "some/relative/target" + ); + + let file = dir.path().join("real.txt"); + std::fs::write(&file, b"x").expect("write the fixture file"); + let error = readlink(text(&file)).expect_err("a plain file is not a link"); + assert_eq!(error.code.as_deref(), Some("EINVAL")); + } + + /// Windows is skipped because creating a symlink there needs developer mode; + /// the OS error would be the correct result, not a test failure. + #[cfg(unix)] + #[test] + fn symlink_round_trips_through_readlink() { + let dir = tempdir(); + let target = dir.path().join("target.txt"); + std::fs::write(&target, b"x").expect("write the fixture file"); + let link = dir.path().join("link.txt"); + + symlink(text(&target), text(&link), None).expect("symlink"); + assert_eq!(readlink(text(&link)).expect("readlink"), text(&target)); + assert_eq!(std::fs::read(&link).expect("through the link"), b"x"); + } + + #[test] + fn symlinking_onto_an_existing_path_is_eexist() { + let dir = tempdir(); + let one = dir.path().join("one.txt"); + let two = dir.path().join("two.txt"); + std::fs::write(&one, b"1").expect("write"); + std::fs::write(&two, b"2").expect("write"); + + let error = symlink(text(&one), text(&two), None).expect_err("the name is taken"); + #[cfg(unix)] + assert_eq!(error.code.as_deref(), Some("EEXIST")); + // Windows reports the same collision, but through a Win32 code whose + // kind is "already exists"; only the presence of an error is portable. + #[cfg(not(unix))] + assert!(error.code.is_some()); + } +} diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/module.rs b/vendor/react-native-gpui/modules/fs/desktop/src/module.rs new file mode 100644 index 0000000000..a20f253dd4 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/module.rs @@ -0,0 +1,187 @@ +//! Shared state and helpers for the file system module. +//! +//! The trait's methods are split across sibling files by concern — `files`, +//! `dirs`, `handles`, `paths`, `dialogs`, `watching` — because they are large and +//! largely independent. Rust allows the trait to be implemented in several files +//! of one crate, so each file owns its own `impl` block. +//! +//! Everything they share is here: the open file table, the flag translation, and +//! the conversions the bridge's `f64` arguments need. + +use std::fs::File; +use std::sync::atomic::AtomicU32; +use std::sync::Mutex; + +use rn_gpui_module::{ModuleError, ModuleThread}; + +use crate::NativeFsEvents; + +use crate::errors; + +/// Flags, as `node:fs` numbers them: Node's table, not the platform's, so these +/// are translated rather than passed through to `OpenOptions`. +pub mod flags { + pub const RDONLY: i32 = 0; + pub const WRONLY: i32 = 1; + pub const RDWR: i32 = 2; + pub const CREAT: i32 = 64; + pub const EXCL: i32 = 128; + pub const TRUNC: i32 = 512; + pub const APPEND: i32 = 1024; +} + +/// Access modes for `access`, matching `node:fs.constants`. +pub mod access { + pub const F_OK: i32 = 0; + pub const X_OK: i32 = 1; + pub const W_OK: i32 = 2; + pub const R_OK: i32 = 4; +} + +/// The kind numbers the JS layer turns into `Stats` predicates. +pub mod kind { + pub const FILE: f64 = 0.0; + pub const DIRECTORY: f64 = 1.0; + pub const SYMLINK: f64 = 2.0; + pub const OTHER: f64 = 3.0; +} + +pub struct RNFsModule { + pub events: NativeFsEvents, + /// Open files, keyed by the number JS holds. The table belongs to the + /// instance, so a reload closes every descriptor when it drops. + /// + /// Each entry records whether the descriptor was opened writable. The OS + /// reports a write to a read-only handle as `EBADF` on Unix and `EACCES` on + /// Windows, and Node reports `EBADF` on both, so the answer is taken from what + /// the handle was opened with rather than from the errno. + pub files: Mutex>, + /// Never reused, so a stale `fd` from JS fails instead of closing an + /// unrelated file. + pub next_fd: AtomicU32, + /// Live watchers, keyed the same way. + pub watchers: Mutex>, + pub next_watcher: AtomicU32, +} + +impl RNFsModule { + pub fn create(events: NativeFsEvents) -> Self { + Self { + events, + files: Mutex::new(std::collections::HashMap::new()), + next_fd: AtomicU32::new(1), + watchers: Mutex::new(std::collections::HashMap::new()), + next_watcher: AtomicU32::new(1), + } + } + + pub fn take_fd(&self) -> u32 { + self.next_fd + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } + + pub fn take_watcher_id(&self) -> u32 { + self.next_watcher + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } + + /// A clone of an open file, sharing its offset — which is what a descriptor + /// is. Holding the lock only for the lookup keeps a slow write from blocking + /// every other call on this thread. + pub fn file(&self, fd: f64) -> Result { + self.file_and_writability(fd).map(|(file, _)| file) + } + + /// The handle and whether it can be written to. + pub fn file_and_writability(&self, fd: f64) -> Result<(File, bool), ModuleError> { + let id = descriptor(fd)?; + let files = self.files.lock().unwrap_or_else(|e| e.into_inner()); + let (file, writable) = files.get(&id).ok_or_else(|| bad_fd(id))?; + let cloned = file + .try_clone() + .map_err(|error| errors::from_io(error, &id.to_string()))?; + Ok((cloned, *writable)) + } +} + +/// The error Node raises for a descriptor it does not know. +pub fn bad_fd(id: u32) -> ModuleError { + errors::invalid("EBADF", format!("EBADF: bad file descriptor, {id}")) +} + +/// A descriptor from the bridge. Anything but a non-negative integer is a +/// programmer error, so it reports as a bad descriptor rather than a file error. +pub fn descriptor(value: f64) -> Result { + if value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= u32::MAX as f64 { + Ok(value as u32) + } else { + Err(bad_fd(0)) + } +} + +/// A byte length from the bridge. +pub fn length(value: f64) -> Result { + if value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= u32::MAX as f64 { + Ok(value as usize) + } else { + Err(errors::invalid( + "EINVAL", + format!("EINVAL: invalid argument, length {value}"), + )) + } +} + +/// A permission bitmask, clamped to the bits `std::fs` accepts. Zero means the +/// caller passed nothing, which Node treats as `0o666`. +pub fn mode(value: f64) -> u32 { + if value.is_finite() && value > 0.0 { + value as u32 & 0o7777 + } else { + 0o666 + } +} + +/// `node:fs` numeric flags as `OpenOptions`. +pub fn open_options(flags_value: f64, mode: u32) -> std::fs::OpenOptions { + let flags_value = flags_value as i32; + let mut options = std::fs::OpenOptions::new(); + // Bit tests, not `matches!`: `matches!(value, A | B)` is the or-pattern + // `value == A || value == B`, which for Node's flags asks whether the whole + // mask equals 0 or 2. `'w'` is `WRONLY | CREAT | TRUNC` (577), so that test + // was false and every write opened read-only and failed `EINVAL`. + // `RDONLY` is zero, so "read" is the absence of `WRONLY` rather than a bit. + options + .read(flags_value & flags::WRONLY == 0) + .write(flags_value & (flags::WRONLY | flags::RDWR) != 0) + .create(flags_value & flags::CREAT != 0) + .create_new(flags_value & flags::EXCL != 0) + .truncate(flags_value & flags::TRUNC != 0) + .append(flags_value & flags::APPEND != 0); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt as _; + options.mode(mode); + } + #[cfg(not(unix))] + let _ = mode; + options +} + +/// Milliseconds since the epoch, as the bridge sends them. +pub fn system_time(ms: f64) -> std::time::SystemTime { + if ms.is_finite() && ms >= 0.0 { + std::time::UNIX_EPOCH + std::time::Duration::from_millis(ms as u64) + } else { + std::time::UNIX_EPOCH + } +} + +/// A `SystemTime` as milliseconds, which is what `StatResult` carries. +pub fn millis(time: std::time::SystemTime) -> f64 { + time.duration_since(std::time::UNIX_EPOCH) + .map(|elapsed| elapsed.as_millis() as f64) + .unwrap_or(0.0) +} + +// The spec says `THREAD = Own`; this keeps the module compiled against that. +const _: ModuleThread = ModuleThread::Own; diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/paths.rs b/vendor/react-native-gpui/modules/fs/desktop/src/paths.rs new file mode 100644 index 0000000000..f26400a9b0 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/paths.rs @@ -0,0 +1,231 @@ +//! The well-known folders, as one `KnownPaths` object. +//! +//! `dirs` answers for the folders every platform has. The per-application three +//! — `appData`, `appCache`, `appLogs` — are this module's own convention, because +//! the host does not yet tell modules which app they belong to; see +//! `app_id` below. All three are created on the first read, as the design asks, +//! so a caller can write a log file without a `mkdir` first. + +use std::path::{Path, PathBuf}; + +use rn_gpui_module::ModuleError; + +use crate::KnownPaths; + +/// The public folders plus the three per-application ones, created if needed. +/// +/// Nothing here fails: a folder that does not exist is reported as the temp +/// directory instead. The design treats these as conveniences, and a caller +/// asking where to put a log file is better served by a working stand-in than by +/// a thrown error it cannot recover from — the alternative is a crash on a +/// system where, say, `Desktop` was never created. +pub fn known_paths() -> Result { + let temp = std::env::temp_dir(); + let id = app_id(); + + let app_data = dirs::data_dir().map(|dir| dir.join(&id)); + let app_cache = dirs::cache_dir().map(|dir| dir.join(&id)); + let app_logs = logs_dir(&id); + + Ok(KnownPaths { + home: fallback(dirs::home_dir(), &temp), + documents: fallback(dirs::document_dir(), &temp), + downloads: fallback(dirs::download_dir(), &temp), + desktop: fallback(dirs::desktop_dir(), &temp), + temp: path_string(&temp), + app_data: ensure_directory(app_data, &temp), + app_cache: ensure_directory(app_cache, &temp), + app_logs: ensure_directory(app_logs, &temp), + }) +} + +/// A folder as a string, with the temp directory standing in when the platform +/// cannot name it. +fn fallback(found: Option, temp: &Path) -> String { + path_string(found.as_deref().unwrap_or(temp)) +} + +/// A path as `KnownPaths` carries it. A non-UTF-8 path cannot be a JS string, so +/// the lossy form is used rather than failing: it is still a path the platform +/// will resolve, and the folder is only a suggestion to the caller. +fn path_string(path: &Path) -> String { + path.to_string_lossy().into_owned() +} + +/// Creates a directory that the returned path promises exists, falling back to +/// the temp directory if it cannot be created. +/// +/// A failure here means the platform gave no base directory at all, or that it +/// exists but is not writable; temp is the one directory a process can normally +/// count on, so the caller still gets somewhere to write. +fn ensure_directory(found: Option, temp: &Path) -> String { + let Some(directory) = found else { + return path_string(temp); + }; + match std::fs::create_dir_all(&directory) { + Ok(()) => path_string(&directory), + Err(_) => path_string(temp), + } +} + +/// Where `appLogs` lives, which is the one app folder whose convention differs +/// by platform: macOS keeps logs under `~/Library/Logs`, the others under the +/// data directory. Both get the app id as their last component so two apps do +/// not share a log folder. +fn logs_dir(id: &str) -> Option { + #[cfg(target_os = "macos")] + { + dirs::home_dir().map(|home| home.join("Library").join("Logs").join(id)) + } + #[cfg(not(target_os = "macos"))] + { + dirs::data_dir().map(|dir| dir.join(id).join("logs")) + } +} + +/// The id the three per-app folders are namespaced by. +/// +/// This is a **placeholder**. The design wants these paths to be per-app and +/// says the host already knows the id (`DesktopAppConfig::app_id`), but no host +/// function exposes it to modules yet: `host_info().app_id` does not exist. Until +/// that API lands, `RN_GPUI_APP_ID` is honoured when the app sets it, and the +/// executable's file stem is the fallback — stable for a given binary, which is +/// enough to keep the folders apart and across runs. +/// +/// When `host_info().app_id` is added, this function should read it and the two +/// fallbacks should go, because a renamed binary would otherwise move the app's +/// data and cache out from under the user. +fn app_id() -> String { + let from_environment = std::env::var("RN_GPUI_APP_ID").ok(); + let from_executable = std::env::current_exe().ok().and_then(|exe| { + exe.file_stem() + .map(|stem| stem.to_string_lossy().into_owned()) + }); + pick_app_id(from_environment.as_deref(), from_executable.as_deref()) +} + +/// The selection half of `app_id`, split out so the precedence and the fallback +/// can be tested without touching the environment, which other tests share. +fn pick_app_id(from_environment: Option<&str>, from_executable: Option<&str>) -> String { + [from_environment, from_executable] + .into_iter() + .flatten() + .map(sanitise) + .find(|id| !id.is_empty()) + // A process whose executable has no usable name still needs folders, and + // the name only has to be stable, not accurate. + .unwrap_or_else(|| "rn-gpui-app".to_string()) +} + +/// Strips the characters that cannot appear in a directory name on any of the +/// three platforms, so an id from the environment cannot escape its parent +/// directory or fail `create_dir_all` on Windows. +/// +/// Leading and trailing dots and dashes go too: an id that was only separators +/// sanitises to nothing rather than to a directory named `-`, and one that began +/// with `..` cannot end up looking like a parent reference. +fn sanitise(id: &str) -> String { + id.trim() + .chars() + .map(|character| match character { + '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '-', + other => other, + }) + .collect::() + .trim_matches(|character| character == '.' || character == '-') + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_known_path_is_a_non_empty_string() { + let paths = known_paths().expect("known paths"); + for (name, value) in [ + ("home", &paths.home), + ("documents", &paths.documents), + ("downloads", &paths.downloads), + ("desktop", &paths.desktop), + ("temp", &paths.temp), + ("appData", &paths.app_data), + ("appCache", &paths.app_cache), + ("appLogs", &paths.app_logs), + ] { + assert!(!value.is_empty(), "{name} was empty"); + } + } + + #[test] + fn the_app_folders_exist_after_the_first_read() { + let paths = known_paths().expect("known paths"); + for (name, value) in [ + ("appData", &paths.app_data), + ("appCache", &paths.app_cache), + ("appLogs", &paths.app_logs), + ] { + assert!( + Path::new(value).is_dir(), + "{name} was not created at {value}" + ); + } + } + + #[test] + fn the_app_folders_are_distinct() { + let paths = known_paths().expect("known paths"); + let app_data = Path::new(&paths.app_data); + let app_cache = Path::new(&paths.app_cache); + let app_logs = Path::new(&paths.app_logs); + + assert_ne!(app_data, app_cache); + assert_ne!(app_data, app_logs); + assert_ne!(app_cache, app_logs); + } + + #[test] + fn a_second_read_returns_the_same_folders() { + // Creating a directory that already exists has to keep succeeding, since + // the JS layer may call `getPaths()` more than once per run. + let first = known_paths().expect("known paths"); + let second = known_paths().expect("known paths"); + assert_eq!(first.app_data, second.app_data); + assert_eq!(first.app_cache, second.app_cache); + assert_eq!(first.app_logs, second.app_logs); + } + + #[test] + fn an_id_from_the_environment_names_the_app_folders() { + // Only the pure part is asserted: mutating the environment would race + // with the other tests in this module, which read it concurrently. + assert_eq!(sanitise("my-app"), "my-app"); + assert_eq!(sanitise(" ../escape "), "escape"); + assert_eq!(sanitise("a/b\\c"), "a-b-c"); + assert_eq!(sanitise(" "), ""); + } + + #[test] + fn the_environment_id_wins_over_the_executable_name() { + assert_eq!(pick_app_id(Some("from-env"), Some("from-exe")), "from-env"); + } + + #[test] + fn an_unusable_environment_id_falls_through_to_the_executable_name() { + assert_eq!(pick_app_id(Some(" "), Some("from-exe")), "from-exe"); + // A separator-only id would escape its parent, which `sanitise` turns + // into a name rather than a path. + assert_eq!(pick_app_id(Some("/"), Some("from-exe")), "from-exe"); + } + + #[test] + fn no_id_at_all_still_yields_a_stable_name() { + assert_eq!(pick_app_id(None, None), "rn-gpui-app"); + assert_eq!(pick_app_id(Some(""), Some("")), "rn-gpui-app"); + } + + #[test] + fn a_hostile_executable_name_cannot_escape_the_base_directory() { + assert_eq!(pick_app_id(None, Some("../../etc/passwd")), "etc-passwd"); + } +} diff --git a/vendor/react-native-gpui/modules/fs/desktop/src/watching.rs b/vendor/react-native-gpui/modules/fs/desktop/src/watching.rs new file mode 100644 index 0000000000..356db5f517 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/desktop/src/watching.rs @@ -0,0 +1,392 @@ +//! The watcher behind `watch` and `unwatch`. +//! +//! `notify` gives each watcher a thread of its own and calls the handler on it. +//! That handler cannot borrow the module — it may outlive any single call and +//! runs on a thread the module does not own — so it is handed a clone of the +//! event emitter and an `Arc` of the debounce table instead. The module only +//! stores the `RecommendedWatcher`; dropping that value stops the OS watch, and +//! with it the thread, which is how a JS reload closes every watcher at once. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use notify::event::EventKind; +use notify::Watcher as _; +use notify::{RecommendedWatcher, RecursiveMode}; +use rn_gpui_module::ModuleError; + +use crate::{errors, RNFsModule, WatchEvent}; + +/// How long a path stays quiet after being reported. +/// +/// An editor saving a file usually writes it several times in a burst (write, +/// rename over the original, a truncated open), and a recursive watch on a build +/// directory can see dozens of events for one logical change. Fifty milliseconds +/// collapses a burst into the single event a caller can act on without making a +/// genuinely repeated edit invisible. +const DEBOUNCE: Duration = Duration::from_millis(50); + +/// An installed watch, kept alive by the module's table. +/// +/// The field is never read: it exists so that removing the entry drops it, which +/// is what stops notify's thread and the platform watch. +pub struct Watcher { + _watcher: RecommendedWatcher, +} + +/// What was last reported for a path, and when. +#[derive(Clone, Copy)] +struct LastReport { + at: Instant, + kind: EventKindTag, +} + +/// Which of the three stable names an event carries. +/// +/// A copy of the mapping `kind_name` produces, so the debounce can compare two +/// events without allocating the string it will not send. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +enum EventKindTag { + Create, + Modify, + Remove, + Other, +} + +/// Per-path timestamps of the last event that was reported. +/// +/// Shared with the handler through an `Arc` because the handler owns its state +/// for as long as the watcher lives, which is longer than any borrow of the +/// module. Paths that fall out of the window are pruned on every report, so a +/// long-running recursive watch cannot grow this without bound. +/// +/// The rule, in one sentence: **a burst is collapsed per path and per kind, and a +/// removal is never collapsed away.** Keeping only the first event of a burst +/// lost the end of it — a create followed by a remove reported only `create`, so +/// a caller was told about a file that no longer existed. Two adjustments fix +/// that without giving up the collapsing that makes an editor's write burst +/// readable: +/// +/// - a path's window is per kind, so a `remove` after a `create` is a different +/// event rather than a suppressed one; +/// - a removal always reports, whatever the window, because it is the last state +/// a path can reach and swallowing it leaves the caller's view permanently +/// wrong. The cost is a possible duplicate removal for one logical deletion, +/// which a watcher callback can tolerate; a missing one it cannot. +#[derive(Default)] +struct Debounce { + last: Mutex>, +} + +impl Debounce { + /// Whether this event should be delivered now. + /// + /// Takes `now` rather than reading the clock so the window can be tested + /// without sleeping. + fn should_report_at(&self, path: &Path, kind: EventKindTag, now: Instant) -> bool { + let mut last = self.last.lock().unwrap_or_else(|error| error.into_inner()); + let key = (path.to_path_buf(), kind); + let report = match last.get(&key).copied() { + Some(previous) => { + kind == EventKindTag::Remove || now.saturating_duration_since(previous) >= DEBOUNCE + } + None => true, + }; + if report { + last.retain(|_, previous| now.saturating_duration_since(*previous) < DEBOUNCE); + last.insert(key, now); + } + report + } + + fn should_report(&self, path: &Path, kind: EventKindTag) -> bool { + self.should_report_at(path, kind, Instant::now()) + } +} + +/// The stable string the JS layer matches on. +/// +/// Everything that is not a create, modify or remove — access events, and the +/// catch-all notify reports when a backend cannot be more precise — becomes +/// `other` rather than leaking a platform-specific spelling. +fn kind_name(kind: &EventKind) -> &'static str { + match kind { + EventKind::Create(_) => "create", + EventKind::Modify(_) => "modify", + EventKind::Remove(_) => "remove", + _ => "other", + } +} + +/// The tag `kind_name` corresponds to, for the debounce to compare by. +fn kind_tag(kind: &EventKind) -> EventKindTag { + match kind { + EventKind::Create(_) => EventKindTag::Create, + EventKind::Modify(_) => EventKindTag::Modify, + EventKind::Remove(_) => EventKindTag::Remove, + _ => EventKindTag::Other, + } +} + +/// An event path as the caller should see it. +/// +/// Node reports the path relative to the watch root, and a name alone for a +/// single-file watch. The platform reports an absolute path, and on macOS a +/// *canonical* one: FSEvents resolves `/var` to `/private/var`, so a plain string +/// prefix check against the path the caller gave strips nothing and the callback +/// receives an absolute path. Both roots are therefore tried, and the given one +/// last so a caller watching through a symlink still gets a relative name. +fn relative_event_path(path: &Path, roots: &[PathBuf], watching_file: bool) -> String { + if watching_file { + return path + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.to_string_lossy().into_owned()); + } + for root in roots { + if let Ok(rest) = path.strip_prefix(root) { + // Normalised to `/`: Node reports relative paths with forward + // slashes on every platform, and a Windows caller would otherwise get + // `nested\file.txt` where Node gives `nested/file.txt`. + return rest.to_string_lossy().replace('\\', "/"); + } + } + path.to_string_lossy().into_owned() +} + +/// A notify failure as the error JS expects. +/// +/// Most failures wrap an `io::Error` that already maps through `errors::from_io`; +/// the ones that do not are described well enough by notify to be reported with +/// the closest Node code. +fn watcher_error(error: notify::Error, path: &str) -> ModuleError { + use notify::ErrorKind; + let io = match error.kind { + ErrorKind::Io(error) => error, + ErrorKind::PathNotFound => std::io::Error::new(std::io::ErrorKind::NotFound, "not found"), + ErrorKind::MaxFilesWatch => { + std::io::Error::new(std::io::ErrorKind::Other, "OS file watch limit reached") + } + other => std::io::Error::new(std::io::ErrorKind::Other, format!("{other:?}")), + }; + errors::from_io(io, path) +} + +/// Starts watching `path`, returning the id events carry. +pub fn watch(module: &RNFsModule, path: String, recursive: bool) -> Result { + // Node fails a watch on a missing path immediately, and so must this: the + // platform watchers otherwise accept the path and then go silent, which is + // indistinguishable at the call site from a directory nothing ever touches. + let target = PathBuf::from(&path); + if !target.exists() { + return Err(errors::from_io( + std::io::Error::new(std::io::ErrorKind::NotFound, "not found"), + &path, + )); + } + + let id = module.take_watcher_id(); + let debounce = Arc::new(Debounce::default()); + let events = module.events.clone(); + // The canonical root is what macOS actually reports inside, and the given + // root is what the caller passed; keeping both means the relative name + // survives either spelling. + let watching_file = target.is_file(); + let roots: Vec = { + let mut roots = Vec::new(); + if let Ok(canonical) = target.canonicalize() { + roots.push(canonical); + } + roots.push(target.clone()); + roots + }; + let handler = move |result: notify::Result| { + // A backend error has nowhere to go: it is not tied to a call, and + // reporting it through the event stream would make it look like a file + // change. notify's thread keeps running, so later events still arrive. + let Ok(event) = result else { return }; + let kind = kind_name(&event.kind); + let tag = kind_tag(&event.kind); + // One platform event can name several paths — a directory move, for + // instance — and a caller wants one delivery per path. + for path in &event.paths { + if !debounce.should_report(path, tag) { + continue; + } + events.on_watch_event(WatchEvent { + id: f64::from(id), + kind: kind.to_string(), + path: relative_event_path(path, &roots, watching_file), + }); + } + }; + + let mut watcher = + notify::recommended_watcher(handler).map_err(|error| watcher_error(error, &path))?; + let mode = if recursive { + RecursiveMode::Recursive + } else { + RecursiveMode::NonRecursive + }; + watcher + .watch(&target, mode) + .map_err(|error| watcher_error(error, &path))?; + + module + .watchers + .lock() + .unwrap_or_else(|error| error.into_inner()) + .insert(id, Watcher { _watcher: watcher }); + Ok(f64::from(id)) +} + +/// Stops a watcher. An id that is already gone is not a failure: JS may close a +/// watcher twice, or close one whose module has already dropped it. +pub fn unwatch(module: &RNFsModule, id: f64) { + let Ok(id) = crate::module::descriptor(id) else { + return; + }; + // Removing the entry drops the watcher, which stops notify's thread. + module + .watchers + .lock() + .unwrap_or_else(|error| error.into_inner()) + .remove(&id); +} + +#[cfg(test)] +mod tests { + use super::*; + use notify::event::{CreateKind, ModifyKind, RemoveKind}; + + #[test] + fn event_kinds_map_to_stable_names() { + assert_eq!(kind_name(&EventKind::Create(CreateKind::File)), "create"); + assert_eq!(kind_name(&EventKind::Create(CreateKind::Any)), "create"); + assert_eq!(kind_name(&EventKind::Modify(ModifyKind::Any)), "modify"); + assert_eq!(kind_name(&EventKind::Remove(RemoveKind::Any)), "remove"); + assert_eq!(kind_name(&EventKind::Any), "other"); + } + + #[test] + fn the_first_event_for_a_path_is_reported() { + let debounce = Debounce::default(); + let now = Instant::now(); + assert!(debounce.should_report_at(Path::new("/a"), EventKindTag::Modify, now)); + } + + #[test] + fn a_burst_is_collapsed_into_one_event() { + let debounce = Debounce::default(); + let start = Instant::now(); + assert!(debounce.should_report_at(Path::new("/a"), EventKindTag::Modify, start)); + assert!(!debounce.should_report_at( + Path::new("/a"), + EventKindTag::Modify, + start + Duration::from_millis(10) + )); + assert!(!debounce.should_report_at( + Path::new("/a"), + EventKindTag::Modify, + start + Duration::from_millis(49) + )); + assert!(debounce.should_report_at( + Path::new("/a"), + EventKindTag::Modify, + start + Duration::from_millis(50) + )); + } + + #[test] + fn a_remove_inside_a_burst_is_not_swallowed() { + let debounce = Debounce::default(); + let start = Instant::now(); + let path = Path::new("/a"); + // A create and the removal that follows it are two states of the path, + // not one event seen twice: reporting only the create tells a caller + // about a file that is already gone. + assert!(debounce.should_report_at(path, EventKindTag::Create, start)); + assert!(debounce.should_report_at( + path, + EventKindTag::Remove, + start + Duration::from_millis(5) + )); + // A second removal for the same deletion may arrive; a missing one may + // not, so this is deliberately the lenient side of the trade. + assert!(debounce.should_report_at( + path, + EventKindTag::Remove, + start + Duration::from_millis(10) + )); + } + + #[test] + fn a_modify_burst_still_collapses() { + let debounce = Debounce::default(); + let start = Instant::now(); + let path = Path::new("/a"); + assert!(debounce.should_report_at(path, EventKindTag::Modify, start)); + // Two writes a millisecond apart are one logical change. + assert!(!debounce.should_report_at( + path, + EventKindTag::Modify, + start + Duration::from_millis(1) + )); + } + + #[test] + fn a_canonical_root_still_yields_a_relative_name() { + // macOS reports canonical paths, so a watch on `/var/...` sees + // `/private/var/...`. Both roots are tried, and the caller gets a name + // relative to the one they asked about either way. + let canonical = PathBuf::from("/private/var/folders/x/tmp"); + let given = PathBuf::from("/var/folders/x/tmp"); + let roots = vec![canonical.clone(), given]; + let event = canonical.join("nested").join("file.txt"); + assert_eq!( + relative_event_path(&event, &roots, false), + "nested/file.txt" + ); + // A path outside every root is passed through rather than mangled. + assert_eq!( + relative_event_path(Path::new("/elsewhere/file.txt"), &roots, false), + "/elsewhere/file.txt" + ); + // Watching a single file reports its name, which is what Node does. + assert_eq!(relative_event_path(&event, &roots, true), "file.txt"); + } + + #[test] + fn debouncing_is_per_path() { + let debounce = Debounce::default(); + let start = Instant::now(); + assert!(debounce.should_report_at(Path::new("/a"), EventKindTag::Modify, start)); + assert!(debounce.should_report_at( + Path::new("/b"), + EventKindTag::Modify, + start + Duration::from_millis(1) + )); + assert!(!debounce.should_report_at( + Path::new("/a"), + EventKindTag::Modify, + start + Duration::from_millis(2) + )); + } + + #[test] + fn paths_outside_the_window_are_forgotten() { + let debounce = Debounce::default(); + let start = Instant::now(); + assert!(debounce.should_report_at(Path::new("/a"), EventKindTag::Modify, start)); + assert!(debounce.should_report_at( + Path::new("/b"), + EventKindTag::Modify, + start + Duration::from_millis(60) + )); + let last = debounce.last.lock().unwrap(); + assert_eq!(last.len(), 1, "the stale path should have been pruned"); + assert!(last.contains_key(&(PathBuf::from("/b"), EventKindTag::Modify))); + } +} diff --git a/vendor/react-native-gpui/modules/fs/package.json b/vendor/react-native-gpui/modules/fs/package.json new file mode 100644 index 0000000000..666bc9a52f --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/package.json @@ -0,0 +1,19 @@ +{ + "name": "@react-native-gpui/fs", + "version": "0.1.0", + "main": "src/index.ts", + "files": [ + "src", + "desktop", + "react-native.config.js", + "README.md" + ], + "codegenConfig": { + "name": "RNFsModuleSpec", + "type": "modules", + "jsSrcsDir": "src" + }, + "peerDependencies": { + "react-native": ">=0.76" + } +} diff --git a/vendor/react-native-gpui/modules/fs/react-native.config.js b/vendor/react-native-gpui/modules/fs/react-native.config.js new file mode 100644 index 0000000000..3ef720686e --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/react-native.config.js @@ -0,0 +1,9 @@ +module.exports = { + dependency: { + platforms: { + desktop: { + crate: 'rn-gpui-fs', + }, + }, + }, +}; diff --git a/vendor/react-native-gpui/modules/fs/src/NativeFs.ts b/vendor/react-native-gpui/modules/fs/src/NativeFs.ts new file mode 100644 index 0000000000..7d22e85598 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/src/NativeFs.ts @@ -0,0 +1,162 @@ +import type { CodegenTypes, TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +/** + * How a path entry is classified: 0 file, 1 directory, 2 symlink, 3 other. + * + * A plain number rather than a string or enum so it crosses the bridge without a + * lookup table and survives being read back out of a plain object in JS. + */ +export type FileKind = number; + +export type StatResult = { + size: number; + mode: number; + kind: FileKind; + atimeMs: number; + mtimeMs: number; + ctimeMs: number; + birthtimeMs: number; + dev: number; + ino: number; + nlink: number; + uid: number; + gid: number; +}; + +export type DirEntry = { + name: string; + kind: FileKind; + parentPath: string; +}; + +export type DialogFilter = { + name: string; + extensions: Array; +}; + +export type OpenDialogOptions = { + title?: string; + defaultPath?: string; + multiple: boolean; + directory: boolean; + filters: Array; +}; + +export type SaveDialogOptions = { + title?: string; + defaultPath?: string; + defaultName?: string; + filters: Array; +}; + +export type KnownPaths = { + home: string; + documents: string; + downloads: string; + desktop: string; + temp: string; + appData: string; + appCache: string; + appLogs: string; +}; + +export type WatchEvent = { + id: number; + kind: string; + path: string; +}; + +/** + * The file system module: one instance, on its own thread, so a slow file never + * blocks JS or the UI. + * + * A method that returns a value runs on the JS thread; `void` and `Promise` + * methods run on the module's thread. The writers below therefore return + * `boolean` — a `void` method would be moved off the JS thread, which is wrong + * for a `*Sync` call. They always return true and throw on failure. + */ +export interface Spec extends TurboModule { + // Text is separate from bytes so a `utf8` read or write needs no buffer. + readFile(path: string): Promise; + readTextFile(path: string): Promise; + writeFile( + path: string, + data: ArrayBuffer, + flags: number, + mode: number, + ): Promise; + writeTextFile( + path: string, + data: string, + flags: number, + mode: number, + ): Promise; + + readdir(path: string, recursive: boolean): Promise>; + // `followSymlinks` false is `lstat`. + stat(path: string, followSymlinks: boolean): Promise; + mkdir(path: string, recursive: boolean, mode: number): Promise; + rm(path: string, recursive: boolean, force: boolean): Promise; + rename(from: string, to: string): Promise; + copy(from: string, to: string, recursive: boolean): Promise; + access(path: string, mode: number): Promise; + realpath(path: string): Promise; + readlink(path: string): Promise; + // `type` is Node's, and only Windows acts on it: `'dir'` and `'junction'` + // create a directory link, `null` decides from the resolved target, and a + // plain `'file'` is a file link. Unix ignores it, because the syscall does. + // Without it a Windows directory link was created as a file link and could + // not be traversed. + symlink(target: string, path: string, type: string | null): Promise; + truncate(path: string, length: number): Promise; + utimes(path: string, atimeMs: number, mtimeMs: number): Promise; + chmod(path: string, mode: number): Promise; + mkdtemp(prefix: string): Promise; + + // File handles. `position` -1 means "at the current offset". + open(path: string, flags: number, mode: number): Promise; + read(fd: number, length: number, position: number): Promise; + write(fd: number, data: ArrayBuffer, position: number): Promise; + fstat(fd: number): Promise; + fsync(fd: number): Promise; + ftruncate(fd: number, length: number): Promise; + close(fd: number): Promise; + + showOpenDialog(options: OpenDialogOptions): Promise>; + showSaveDialog(options: SaveDialogOptions): Promise; + reveal(path: string): Promise; + openExternal(path: string): Promise; + + // Returns the watcher id; events arrive through `onWatchEvent`. + watch(path: string, recursive: boolean): Promise; + unwatch(id: number): void; + + readFileSync(path: string): ArrayBuffer; + readTextFileSync(path: string): string; + writeFileSync( + path: string, + data: ArrayBuffer, + flags: number, + mode: number, + ): boolean; + writeTextFileSync( + path: string, + data: string, + flags: number, + mode: number, + ): boolean; + statSync(path: string, followSymlinks: boolean): StatResult | null; + existsSync(path: string): boolean; + readdirSync(path: string, recursive: boolean): Array; + mkdirSync(path: string, recursive: boolean, mode: number): boolean; + rmSync(path: string, recursive: boolean, force: boolean): boolean; + renameSync(from: string, to: string): boolean; + copyFileSync(from: string, to: string): boolean; + + getPaths(): KnownPaths; + + readonly onWatchEvent: CodegenTypes.EventEmitter; +} + +export default TurboModuleRegistry.getEnforcing('RNFsModule'); diff --git a/vendor/react-native-gpui/modules/fs/src/constants.ts b/vendor/react-native-gpui/modules/fs/src/constants.ts new file mode 100644 index 0000000000..c2740f3faa --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/src/constants.ts @@ -0,0 +1,43 @@ +/** + * `fs.constants`, with Node's numbers. + * + * The `O_*` values are Node's table rather than the platform's `open(2)` flags, + * because that is what the Rust side decodes: it translates them into + * `OpenOptions` instead of passing them through. The numbers here must stay in + * step with `modules/fs/desktop/src/module.rs`, which owns the other half of the + * contract. + */ + +export const constants = { + // `access` modes. + F_OK: 0, + X_OK: 1, + W_OK: 2, + R_OK: 4, + + // `open` flags. + O_RDONLY: 0, + O_WRONLY: 1, + O_RDWR: 2, + O_CREAT: 64, + O_EXCL: 128, + O_TRUNC: 512, + O_APPEND: 1024, + + // `copyFile` modes. + COPYFILE_EXCL: 1, + + // File-type bits, for the predicates on `Stats` that cannot be answered from + // the module's coarse `kind` alone (a socket and a block device are both + // "other" there). + S_IFMT: 0o170000, + S_IFREG: 0o100000, + S_IFDIR: 0o040000, + S_IFLNK: 0o120000, + S_IFBLK: 0o060000, + S_IFCHR: 0o020000, + S_IFIFO: 0o010000, + S_IFSOCK: 0o140000, +} as const; + +export type Constants = typeof constants; diff --git a/vendor/react-native-gpui/modules/fs/src/index.ts b/vendor/react-native-gpui/modules/fs/src/index.ts new file mode 100644 index 0000000000..fe5cefe9a8 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/src/index.ts @@ -0,0 +1,769 @@ +/** + * `@react-native-gpui/fs`: a Node-shaped file API for the desktop app. + * + * The native module underneath is one TurboModule on its own thread with + * `ArrayBuffer` arguments and plain-object results. Everything Node adds on top + * of that — encodings, `Stats` and `Dirent` instances, callback forms, the + * `fs/promises` object — lives in this package's JS layer, so the bridge stays a + * byte mover and the API stays recognisably Node's. + * + * The default export is the `node:fs` object; `dialog`, `paths` and `watch` are + * named exports because they have no `fs` counterpart to hang off. + */ + +import { constants } from './constants'; +import NativeFs from './NativeFs'; +import { + Dirent, + FileHandle, + Stats, + promises, + readFileImpl, + type MkdirOptions, + type OpenOptions, + type RmOptions, + type WriteOptions, +} from './promises'; +import { + decodeString, + flagFrom, + missingPath, + modeFrom, + normalizeEncoding, + relativeEntryName, + toArrayBuffer, + toPath, + withContext, + type BinaryData, + type Encoding, + type NodeError, +} from './shared'; +import { watch } from './watch'; + +export type { + CpOptions, + MkdirOptions, + OpenOptions, + ReaddirOptions, + RmOptions, + TimeLike, + WriteOptions, +} from './promises'; +export type { BinaryData, Encoding, NodeError } from './shared'; +export type { + FSWatcher, + WatchEventType, + WatchListener, + WatchOptions, +} from './watch'; +export { Dirent, Stats, FileHandle, promises, constants, watch }; + +/* -------------------------------------------------------------------------- */ +/* Dialogs */ +/* -------------------------------------------------------------------------- */ + +export interface DialogFilter { + name: string; + extensions: Array; +} + +export interface OpenDialogOptions { + title?: string; + defaultPath?: string; + multiple?: boolean; + directory?: boolean; + filters?: Array; +} + +export interface SaveDialogOptions { + title?: string; + defaultPath?: string; + defaultName?: string; + filters?: Array; +} + +/** + * The native pickers. + * + * Cancelling is not an error: `open` resolves with `[]` and `save` with `null`, + * which is what the design specifies and what libraries pairing the two expect. + */ +export const dialog = { + async open(options: OpenDialogOptions = {}): Promise { + return NativeFs.showOpenDialog({ + title: options.title, + defaultPath: + options.defaultPath === undefined + ? undefined + : toPath(options.defaultPath), + multiple: options.multiple ?? false, + directory: options.directory ?? false, + filters: options.filters ?? [], + }); + }, + + async save(options: SaveDialogOptions = {}): Promise { + return NativeFs.showSaveDialog({ + title: options.title, + defaultPath: + options.defaultPath === undefined + ? undefined + : toPath(options.defaultPath), + defaultName: options.defaultName, + filters: options.filters ?? [], + }); + }, +}; + +/* -------------------------------------------------------------------------- */ +/* Known paths */ +/* -------------------------------------------------------------------------- */ + +/** The well-known directories, as the native side reports them. */ +export interface KnownPaths { + home: string; + documents: string; + downloads: string; + desktop: string; + temp: string; + appData: string; + appCache: string; + appLogs: string; +} + +const KNOWN_PATH_NAMES = [ + 'home', + 'documents', + 'downloads', + 'desktop', + 'temp', + 'appData', + 'appCache', + 'appLogs', +] as const; + +let cachedPaths: KnownPaths | null = null; + +/** + * Reads the known paths once and remembers them. + * + * Lazy rather than at import time so that importing this package cannot fail: the + * native module is only asked when an app actually names a directory, and the + * native side creates the per-app folders on that first read. + */ +function knownPaths(): KnownPaths { + if (cachedPaths === null) { + cachedPaths = NativeFs.getPaths(); + } + return cachedPaths; +} + +/** + * `paths.home`, `paths.documents` and the rest. + * + * Each property is a getter over one cached native read, so destructuring works + * and the object can be exported before the module has been asked anything. + */ +export const paths: KnownPaths = Object.defineProperties( + {}, + Object.fromEntries( + KNOWN_PATH_NAMES.map(name => [ + name, + { enumerable: true, get: () => knownPaths()[name] }, + ]), + ), +) as KnownPaths; + +/* -------------------------------------------------------------------------- */ +/* Sync forms */ +/* -------------------------------------------------------------------------- */ + +function readFileSync(path: string): Uint8Array; +function readFileSync( + path: string, + options: Encoding | { encoding: Encoding }, +): string; +function readFileSync( + path: string, + options?: Encoding | { encoding?: Encoding | null } | null, +): Uint8Array | string { + const target = toPath(path); + const encoding = + typeof options === 'string' ? options : (options?.encoding ?? null); + try { + if (encoding === null) { + return new Uint8Array(NativeFs.readFileSync(target)); + } + const normalized = normalizeEncoding(encoding) ?? 'utf8'; + if (normalized === 'utf8') { + // The dedicated text method decodes in Rust instead of handing JS bytes + // to decode a second time. + return NativeFs.readTextFileSync(target); + } + return decodeString( + new Uint8Array(NativeFs.readFileSync(target)), + normalized, + ); + } catch (error) { + throw withContext(error, 'open', target); + } +} + +function writeFileSync( + path: string, + data: BinaryData, + options?: WriteOptions | Encoding, +): void { + const target = toPath(path); + const opts: WriteOptions = + typeof options === 'string' ? { encoding: options } : (options ?? {}); + const flag = flagFrom( + opts.flag, + constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC, + ); + const mode = modeFrom(opts.mode); + try { + if (typeof data === 'string') { + const encoding = normalizeEncoding(opts.encoding ?? 'utf8') ?? 'utf8'; + if (encoding === 'utf8') { + NativeFs.writeTextFileSync(target, data, flag, mode); + return; + } + NativeFs.writeFileSync(target, toArrayBuffer(data, encoding), flag, mode); + return; + } + NativeFs.writeFileSync(target, toArrayBuffer(data), flag, mode); + } catch (error) { + throw withContext(error, 'open', target); + } +} + +function appendFileSync( + path: string, + data: BinaryData, + options?: WriteOptions | Encoding, +): void { + const opts: WriteOptions = + typeof options === 'string' ? { encoding: options } : (options ?? {}); + writeFileSync(path, data, { + ...opts, + flag: + opts.flag ?? constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND, + }); +} + +function existsSync(path: string): boolean { + try { + return NativeFs.existsSync(toPath(path)); + } catch { + // `existsSync` answers a question rather than reporting a failure, so every + // error — a permission problem as much as a missing path — is a `false`. + return false; + } +} + +function statSync(path: string): Stats { + const target = toPath(path); + try { + const result = NativeFs.statSync(target, true); + if (result === null) { + throw missingPath(target, 'stat'); + } + return new Stats(result); + } catch (error) { + throw withContext(error, 'stat', target); + } +} + +function lstatSync(path: string): Stats { + const target = toPath(path); + try { + const result = NativeFs.statSync(target, false); + if (result === null) { + throw missingPath(target, 'lstat'); + } + return new Stats(result); + } catch (error) { + throw withContext(error, 'lstat', target); + } +} + +function readdirSync( + path: string, + options?: { withFileTypes?: false; recursive?: boolean } | null, +): string[]; +function readdirSync( + path: string, + options: { withFileTypes: true; recursive?: boolean }, +): Dirent[]; +function readdirSync( + path: string, + options?: { withFileTypes?: boolean; recursive?: boolean } | null, +): string[] | Dirent[] { + const target = toPath(path); + try { + const recursive = options?.recursive ?? false; + const entries = NativeFs.readdirSync(target, recursive); + return options?.withFileTypes + ? entries.map(entry => new Dirent(entry)) + : entries.map(entry => + recursive + ? relativeEntryName(target, entry.parentPath, entry.name) + : entry.name, + ); + } catch (error) { + throw withContext(error, 'scandir', target); + } +} + +function mkdirSync(path: string, options?: MkdirOptions | number): void { + const target = toPath(path); + const opts: MkdirOptions = + typeof options === 'number' ? { mode: options } : (options ?? {}); + try { + NativeFs.mkdirSync( + target, + opts.recursive ?? false, + modeFrom(opts.mode, 0o777), + ); + } catch (error) { + throw withContext(error, 'mkdir', target); + } +} + +function rmSync(path: string, options?: RmOptions): void { + const target = toPath(path); + try { + NativeFs.rmSync( + target, + options?.recursive ?? false, + options?.force ?? false, + ); + } catch (error) { + throw withContext(error, 'rm', target); + } +} + +function renameSync(from: string, to: string): void { + const source = toPath(from); + const destination = toPath(to); + try { + NativeFs.renameSync(source, destination); + } catch (error) { + const wrapped = withContext(error, 'rename', source); + // Node's rename error names both ends; the second is `dest`, not `path`. + if (wrapped.dest === undefined) { + wrapped.dest = destination; + } + throw wrapped; + } +} + +function copyFileSync(from: string, to: string): void { + const source = toPath(from); + const destination = toPath(to); + try { + NativeFs.copyFileSync(source, destination); + } catch (error) { + const wrapped = withContext(error, 'copyfile', source); + if (wrapped.dest === undefined) { + wrapped.dest = destination; + } + throw wrapped; + } +} + +/* -------------------------------------------------------------------------- */ +/* Callback forms */ +/* -------------------------------------------------------------------------- */ + +/** + * The callback forms are wrappers over the promise implementations, as the + * design says, so there is one implementation of each operation. + * + * The trailing argument is the callback in every form; the optional argument + * before it is an options object (or an encoding, or a mode) when it is not a + * function. That is the whole of Node's overloading convention here. + */ + +type NodeCallback = (error: NodeError | null, value?: T) => void; + +function callbackOf(args: unknown[]): NodeCallback { + const callback = args[args.length - 1]; + if (typeof callback !== 'function') { + throw new TypeError('A callback function is required'); + } + return callback as NodeCallback; +} + +/** The options argument, if the caller supplied one rather than the callback. */ +function before(args: unknown[], index: number, fallback: T): T { + const value = args[index]; + return value === undefined || typeof value === 'function' + ? fallback + : (value as T); +} + +/** + * Reports a settled promise the Node way. + * + * A rejection that has a callback is delivered to it; nothing is thrown, because + * a throw from here would escape the caller's stack rather than the callback. + */ +function settle(promise: Promise, callback: NodeCallback): void { + promise.then( + value => callback(null, value), + (error: NodeError) => callback(error), + ); +} + +function readFile( + path: string, + callback: NodeCallback, +): void; +function readFile( + path: string, + options: Encoding | { encoding?: Encoding | null } | null, + callback: NodeCallback, +): void; +function readFile(path: string, ...args: unknown[]): void { + // The promise overloads keep the result precise; the callback form goes + // through the coarse alias because at that point the encoding is a runtime + // value and either shape is possible. + const options = before(args, 0, undefined) as + Encoding | { encoding?: Encoding | null } | null | undefined; + settle(readFileImpl(path, options), callbackOf(args)); +} + +function writeFile( + path: string, + data: BinaryData, + callback: NodeCallback, +): void; +function writeFile( + path: string, + data: BinaryData, + options: WriteOptions | Encoding | null, + callback: NodeCallback, +): void; +function writeFile(path: string, data: BinaryData, ...args: unknown[]): void { + settle( + promises.writeFile( + path, + data, + before(args, 0, undefined) as WriteOptions | Encoding | undefined, + ), + callbackOf(args), + ); +} + +function appendFile( + path: string, + data: BinaryData, + callback: NodeCallback, +): void; +function appendFile( + path: string, + data: BinaryData, + options: WriteOptions | Encoding | null, + callback: NodeCallback, +): void; +function appendFile(path: string, data: BinaryData, ...args: unknown[]): void { + settle( + promises.appendFile( + path, + data, + before(args, 0, undefined) as WriteOptions | Encoding | undefined, + ), + callbackOf(args), + ); +} + +function readdir( + path: string, + callback: NodeCallback, +): void; +function readdir( + path: string, + options: { withFileTypes?: boolean; recursive?: boolean } | null, + callback: NodeCallback, +): void; +function readdir(path: string, ...args: unknown[]): void { + const options = before(args, 0, null) as { + withFileTypes?: boolean; + recursive?: boolean; + } | null; + const pending: Promise = options?.withFileTypes + ? promises.readdir(path, { ...options, withFileTypes: true }) + : promises.readdir(path, { + recursive: options?.recursive, + withFileTypes: false, + }); + settle(pending, callbackOf(args)); +} + +function stat(path: string, callback: NodeCallback): void; +function stat(path: string, ...args: unknown[]): void { + settle(promises.stat(path), callbackOf([...args])); +} + +function lstat(path: string, callback: NodeCallback): void; +function lstat(path: string, ...args: unknown[]): void { + settle(promises.lstat(path), callbackOf([...args])); +} + +function mkdir(path: string, callback: NodeCallback): void; +function mkdir( + path: string, + options: MkdirOptions | number | null, + callback: NodeCallback, +): void; +function mkdir(path: string, ...args: unknown[]): void { + settle( + promises.mkdir( + path, + before(args, 0, undefined) as MkdirOptions | number | undefined, + ), + callbackOf(args), + ); +} + +function rm(path: string, callback: NodeCallback): void; +function rm( + path: string, + options: RmOptions | null, + callback: NodeCallback, +): void; +function rm(path: string, ...args: unknown[]): void { + settle( + promises.rm(path, before(args, 0, undefined) as RmOptions | undefined), + callbackOf(args), + ); +} + +function rename(from: string, to: string, callback: NodeCallback): void; +function rename(from: string, to: string, ...args: unknown[]): void { + settle(promises.rename(from, to), callbackOf([...args])); +} + +function copyFile(from: string, to: string, callback: NodeCallback): void; +function copyFile( + from: string, + to: string, + mode: number, + callback: NodeCallback, +): void; +function copyFile(from: string, to: string, ...args: unknown[]): void { + settle( + promises.copyFile( + from, + to, + before(args, 0, undefined) as number | undefined, + ), + callbackOf(args), + ); +} + +function cp(from: string, to: string, callback: NodeCallback): void; +function cp( + from: string, + to: string, + options: { recursive?: boolean } | null, + callback: NodeCallback, +): void; +function cp(from: string, to: string, ...args: unknown[]): void { + settle( + promises.cp( + from, + to, + before(args, 0, undefined) as { recursive?: boolean } | undefined, + ), + callbackOf(args), + ); +} + +function access(path: string, callback: NodeCallback): void; +function access(path: string, mode: number, callback: NodeCallback): void; +function access(path: string, ...args: unknown[]): void { + settle( + promises.access(path, before(args, 0, undefined) as number | undefined), + callbackOf(args), + ); +} + +function realpath(path: string, callback: NodeCallback): void; +function realpath(path: string, ...args: unknown[]): void { + settle(promises.realpath(path), callbackOf([...args])); +} + +function readlink(path: string, callback: NodeCallback): void; +function readlink(path: string, ...args: unknown[]): void { + settle(promises.readlink(path), callbackOf([...args])); +} + +function symlink( + target: string, + path: string, + callback: NodeCallback, +): void; +function symlink( + target: string, + path: string, + type: string, + callback: NodeCallback, +): void; +function symlink(target: string, path: string, ...args: unknown[]): void { + settle( + promises.symlink( + target, + path, + before(args, 0, undefined) as 'file' | 'dir' | 'junction' | undefined, + ), + callbackOf(args), + ); +} + +function truncate(path: string, callback: NodeCallback): void; +function truncate( + path: string, + length: number, + callback: NodeCallback, +): void; +function truncate(path: string, ...args: unknown[]): void { + settle( + promises.truncate(path, before(args, 0, undefined) as number | undefined), + callbackOf(args), + ); +} + +function utimes( + path: string, + atime: Date | number | string, + mtime: Date | number | string, + callback: NodeCallback, +): void; +function utimes( + path: string, + atime: Date | number | string, + mtime: Date | number | string, + ...args: unknown[] +): void { + settle(promises.utimes(path, atime, mtime), callbackOf([...args])); +} + +function chmod( + path: string, + mode: number | string, + callback: NodeCallback, +): void; +function chmod(path: string, mode: number | string, ...args: unknown[]): void { + settle(promises.chmod(path, mode), callbackOf([...args])); +} + +function mkdtemp(prefix: string, callback: NodeCallback): void; +function mkdtemp( + prefix: string, + options: Encoding | { encoding?: Encoding }, + callback: NodeCallback, +): void; +function mkdtemp(prefix: string, ...args: unknown[]): void { + settle( + promises.mkdtemp( + prefix, + before(args, 0, undefined) as + Encoding | { encoding?: Encoding } | undefined, + ), + callbackOf(args), + ); +} + +function open(path: string, callback: NodeCallback): void; +function open( + path: string, + flags: string | number | OpenOptions, + callback: NodeCallback, +): void; +function open( + path: string, + flags: string | number | OpenOptions, + mode: number | string, + callback: NodeCallback, +): void; +function open(path: string, ...args: unknown[]): void { + const flags = before(args, 0, undefined) as + string | number | OpenOptions | undefined; + const mode = before(args, 1, undefined) as number | string | undefined; + settle( + mode === undefined + ? promises.open(path, flags) + : promises.open(path, flags, mode), + callbackOf(args), + ); +} + +function reveal(path: string, callback: NodeCallback): void; +function reveal(path: string, ...args: unknown[]): void { + settle(promises.reveal(path), callbackOf([...args])); +} + +function openExternal(path: string, callback: NodeCallback): void; +function openExternal(path: string, ...args: unknown[]): void { + settle(promises.openExternal(path), callbackOf([...args])); +} + +/* -------------------------------------------------------------------------- */ +/* The `fs` object */ +/* -------------------------------------------------------------------------- */ + +/** + * Node's `fs`, as far as this platform implements it. + * + * The callback forms wrap the promise implementations; the sync forms call the + * bridge's JS-thread methods directly; `promises` is the same object shape Node + * exposes as `fs.promises`. + */ +const fs = { + constants, + promises, + + readFile, + writeFile, + appendFile, + readdir, + stat, + lstat, + mkdir, + rm, + rename, + copyFile, + cp, + access, + realpath, + readlink, + symlink, + truncate, + utimes, + chmod, + mkdtemp, + open, + reveal, + openExternal, + + readFileSync, + writeFileSync, + appendFileSync, + existsSync, + statSync, + lstatSync, + readdirSync, + mkdirSync, + rmSync, + renameSync, + copyFileSync, + + Stats, + Dirent, + FileHandle, +}; + +export default fs; diff --git a/vendor/react-native-gpui/modules/fs/src/promises.ts b/vendor/react-native-gpui/modules/fs/src/promises.ts new file mode 100644 index 0000000000..155f3fb467 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/src/promises.ts @@ -0,0 +1,771 @@ +/** + * `fs.promises`: the Node-shaped asynchronous face of the module. + * + * Every call here is a thin translation. The native side does the work on its own + * thread and reports Node codes, so this layer's job is argument shapes, the + * encodings Node implements above the bytes, and the fields (`syscall`, `path`, + * `dest`) that only a wrapper can know. + */ + +import NativeFs from './NativeFs'; +import { constants } from './constants'; +import { + checkBufferRange, + decodeString, + encodeString, + flagFrom, + modeFrom, + nodeError, + normalizeEncoding, + relativeEntryName, + toArrayBuffer, + toBytes, + toPath, + withContext, + type BinaryData, + type Encoding, + type NormalizedEncoding, +} from './shared'; +import { Dirent, Stats, type DirEntryResult, type StatResult } from './stats'; + +/** An encoding, an options object, or nothing, as Node accepts for reads. */ +export type ReadOptions = + Encoding | { encoding?: Encoding | null } | null | undefined; + +export interface WriteOptions { + encoding?: Encoding | null; + mode?: number | string; + flag?: string | number; +} + +export interface MkdirOptions { + recursive?: boolean; + mode?: number | string; +} + +export interface RmOptions { + recursive?: boolean; + force?: boolean; +} + +export interface ReaddirOptions { + withFileTypes?: boolean; + recursive?: boolean; +} + +export interface CpOptions { + recursive?: boolean; +} + +export interface OpenOptions { + flags?: string | number; + mode?: number | string; +} + +/** The chunk size for a read that has no caller-supplied buffer. */ +const READ_CHUNK = 64 * 1024; + +/** A time as Node accepts one: `Date`, milliseconds, or a date string. */ +export type TimeLike = Date | number | string; + +function toMillis(value: TimeLike, name: string): number { + if (value instanceof Date) { + return value.getTime(); + } + if (typeof value === 'number') { + return value; + } + const parsed = Date.parse(value); + if (Number.isNaN(parsed)) { + throw nodeError( + 'ERR_INVALID_ARG_VALUE', + `The argument '${name}' is invalid time. Received '${value}'`, + ); + } + return parsed; +} + +function encodingOf(options: ReadOptions): NormalizedEncoding | null { + if (typeof options === 'string') { + return normalizeEncoding(options); + } + return normalizeEncoding(options?.encoding ?? null); +} + +/** + * `readFile`, in whichever of the two shapes the caller asked for. + * + * UTF-8 goes through the dedicated text method: the native side decodes it while + * the bytes are still in Rust, so a text read never allocates an `ArrayBuffer` + * that JS then copies. Every other encoding is decoded here, because Node's + * `utf16le`, `latin1`, `base64` and `hex` are JS-level conventions rather than + * something `std::fs` has an opinion about. + */ +function readFile(path: string): Promise; +function readFile( + path: string, + options: { encoding?: null } | null | undefined, +): Promise; +function readFile( + path: string, + options: Encoding | { encoding: Encoding }, +): Promise; +async function readFile( + path: string, + options?: ReadOptions, +): Promise { + const target = toPath(path); + const encoding = encodingOf(options); + try { + if (encoding === 'utf8') { + return await NativeFs.readTextFile(target); + } + const buffer = await NativeFs.readFile(target); + const bytes = new Uint8Array(buffer); + return encoding === null ? bytes : decodeString(bytes, encoding); + } catch (error) { + throw withContext(error, 'open', target); + } +} + +/** + * The same read, typed coarsely. + * + * `readFile`'s public overloads are what a caller sees; the callback layer calls + * through this alias because at that point the encoding is a runtime value and + * the result is genuinely either shape. + */ +export const readFileImpl: ( + path: string, + options?: ReadOptions, +) => Promise = readFile; + +/** + * `writeFile`. + * + * A string is written through `writeTextFile` when it is UTF-8; anything else — + * a different encoding, or binary data — is normalised to an `ArrayBuffer` and + * sent through the byte method. + */ +async function writeFile( + path: string, + data: BinaryData, + options?: WriteOptions | Encoding, +): Promise { + const target = toPath(path); + const opts: WriteOptions = + typeof options === 'string' ? { encoding: options } : (options ?? {}); + const flag = flagFrom( + opts.flag, + constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC, + ); + const mode = modeFrom(opts.mode); + try { + if (typeof data === 'string') { + // `normalizeEncoding` answers `null` only when it is given nothing, and a + // default is supplied here, so the fallback to `utf8` is unreachable — it + // exists to keep the type honest instead of asserting it away. + const encoding = normalizeEncoding(opts.encoding ?? 'utf8') ?? 'utf8'; + if (encoding === 'utf8') { + await NativeFs.writeTextFile(target, data, flag, mode); + } else { + await NativeFs.writeFile( + target, + toArrayBuffer(encodeString(data, encoding)), + flag, + mode, + ); + } + return; + } + await NativeFs.writeFile(target, toArrayBuffer(data), flag, mode); + } catch (error) { + throw withContext(error, 'open', target); + } +} + +async function appendFile( + path: string, + data: BinaryData, + options?: WriteOptions | Encoding, +): Promise { + const opts: WriteOptions = + typeof options === 'string' ? { encoding: options } : (options ?? {}); + return writeFile(path, data, { + ...opts, + flag: + opts.flag ?? constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND, + }); +} + +function readdir( + path: string, + options?: { withFileTypes?: false; recursive?: boolean } | null, +): Promise; +function readdir( + path: string, + options: { withFileTypes: true; recursive?: boolean }, +): Promise; +async function readdir( + path: string, + options?: ReaddirOptions | null, +): Promise { + const target = toPath(path); + try { + const recursive = options?.recursive ?? false; + const entries = await NativeFs.readdir(target, recursive); + return options?.withFileTypes + ? entries.map(entry => new Dirent(entry as DirEntryResult)) + : entries.map(entry => + recursive + ? relativeEntryName(target, entry.parentPath, entry.name) + : entry.name, + ); + } catch (error) { + throw withContext(error, 'scandir', target); + } +} + +async function stat(path: string): Promise { + const target = toPath(path); + try { + return new Stats((await NativeFs.stat(target, true)) as StatResult); + } catch (error) { + throw withContext(error, 'stat', target); + } +} + +async function lstat(path: string): Promise { + const target = toPath(path); + try { + return new Stats((await NativeFs.stat(target, false)) as StatResult); + } catch (error) { + throw withContext(error, 'lstat', target); + } +} + +async function mkdir( + path: string, + options?: MkdirOptions | number, +): Promise { + const target = toPath(path); + const opts: MkdirOptions = + typeof options === 'number' ? { mode: options } : (options ?? {}); + try { + await NativeFs.mkdir( + target, + opts.recursive ?? false, + modeFrom(opts.mode, 0o777), + ); + return undefined; + } catch (error) { + throw withContext(error, 'mkdir', target); + } +} + +async function rm(path: string, options?: RmOptions): Promise { + const target = toPath(path); + try { + await NativeFs.rm( + target, + options?.recursive ?? false, + options?.force ?? false, + ); + } catch (error) { + throw withContext(error, 'rm', target); + } +} + +async function rename(from: string, to: string): Promise { + const source = toPath(from); + const destination = toPath(to); + try { + await NativeFs.rename(source, destination); + } catch (error) { + // Node's rename error names both ends; the second is `dest`, not `path`. + const wrapped = withContext(error, 'rename', source); + if (wrapped.dest === undefined) { + wrapped.dest = destination; + } + throw wrapped; + } +} + +async function copyFile( + from: string, + to: string, + mode?: number, +): Promise { + const source = toPath(from); + const destination = toPath(to); + if (mode !== undefined && (mode & constants.COPYFILE_EXCL) !== 0) { + // The native copy has no exclusive flag, so the check is made here. It is + // advisory — a file created between the two calls still wins — but it gives + // the caller the error Node promises for the ordinary case. + try { + await NativeFs.stat(destination, false); + throw nodeError( + 'EEXIST', + `EEXIST: file already exists, copyfile '${source}' -> '${destination}'`, + ); + } catch (error) { + if ((error as { code?: string }).code !== 'ENOENT') { + throw error; + } + } + } + try { + await NativeFs.copy(source, destination, false); + } catch (error) { + const wrapped = withContext(error, 'copyfile', source); + if (wrapped.dest === undefined) { + wrapped.dest = destination; + } + throw wrapped; + } +} + +async function cp( + from: string, + to: string, + options?: CpOptions, +): Promise { + const source = toPath(from); + const destination = toPath(to); + try { + await NativeFs.copy(source, destination, options?.recursive ?? false); + } catch (error) { + const wrapped = withContext(error, 'cp', source); + if (wrapped.dest === undefined) { + wrapped.dest = destination; + } + throw wrapped; + } +} + +async function access(path: string, mode?: number): Promise { + const target = toPath(path); + try { + await NativeFs.access(target, mode ?? constants.F_OK); + } catch (error) { + throw withContext(error, 'access', target); + } +} + +async function realpath(path: string): Promise { + const target = toPath(path); + try { + return await NativeFs.realpath(target); + } catch (error) { + throw withContext(error, 'realpath', target); + } +} + +async function readlink(path: string): Promise { + const target = toPath(path); + try { + return await NativeFs.readlink(target); + } catch (error) { + throw withContext(error, 'readlink', target); + } +} + +async function symlink( + target: string, + path: string, + type?: 'file' | 'dir' | 'junction', +): Promise { + // `type` is passed through rather than dropped. It is the only way to ask for a + // directory link on Windows, where creating one as a file link leaves a path + // that cannot be traversed; Unix ignores it, as the syscall does. + const link = toPath(path); + try { + await NativeFs.symlink(toPath(target), link, type ?? null); + } catch (error) { + throw withContext(error, 'symlink', link); + } +} + +async function truncate(path: string, length?: number): Promise { + const target = toPath(path); + try { + await NativeFs.truncate(target, length ?? 0); + } catch (error) { + throw withContext(error, 'truncate', target); + } +} + +async function utimes( + path: string, + atime: TimeLike, + mtime: TimeLike, +): Promise { + const target = toPath(path); + try { + await NativeFs.utimes( + target, + toMillis(atime, 'atime'), + toMillis(mtime, 'mtime'), + ); + } catch (error) { + throw withContext(error, 'utime', target); + } +} + +async function chmod(path: string, mode: number | string): Promise { + const target = toPath(path); + try { + await NativeFs.chmod(target, modeFrom(mode)); + } catch (error) { + throw withContext(error, 'chmod', target); + } +} + +async function mkdtemp( + prefix: string, + options?: Encoding | { encoding?: Encoding }, +): Promise { + // The native side creates the directory under the system temp directory and + // returns the path; the encoding only affects how a non-ASCII prefix arrives, + // and every string crosses the bridge as UTF-8. + options; + try { + return await NativeFs.mkdtemp(toPath(prefix)); + } catch (error) { + throw withContext(error, 'mkdtemp', prefix); + } +} + +/** + * A descriptor plus the operations Node puts on `FileHandle`. + * + * Positioned reads and writes do not touch the file's offset, which is what + * makes concurrent chunked I/O safe: two readers on one handle cannot move each + * other's cursor. + */ +export class FileHandle { + /** The native descriptor. */ + readonly fd: number; + private closed = false; + + constructor(fd: number) { + this.fd = fd; + } + + private assertOpen(): void { + if (this.closed) { + throw nodeError('EBADF', `EBADF: bad file descriptor, ${this.fd}`); + } + } + + /** Reads from the current offset when `position` is `-1`. */ + async read(): Promise<{ bytesRead: number; buffer: Uint8Array }>; + async read(options: { + buffer?: Uint8Array; + offset?: number; + length?: number; + position?: number | null; + }): Promise<{ bytesRead: number; buffer: Uint8Array }>; + async read( + buffer: Uint8Array, + offset?: number, + length?: number, + position?: number | null, + ): Promise<{ bytesRead: number; buffer: Uint8Array }>; + /** + * Node's positional form with the buffer implied: `read(size, offset, length, + * position)`. `offset` and `length` are a window into the freshly allocated + * buffer, and a missing `position` reads from the cursor. + */ + async read( + size: number, + offset?: number, + length?: number, + position?: number | null, + ): Promise<{ bytesRead: number; buffer: Uint8Array }>; + async read( + first?: + | number + | Uint8Array + | { + buffer?: Uint8Array; + offset?: number; + length?: number; + position?: number | null; + }, + second?: number, + third?: number, + fourth?: number | null, + ): Promise<{ bytesRead: number; buffer: Uint8Array }> { + this.assertOpen(); + let destination: Uint8Array; + let offset = 0; + let length: number; + let position: number; + // The two no-buffer forms mean "read to the end", which is a different + // operation from a single bounded read and is handled by `readToEnd`. + let toEnd = false; + if (first === undefined) { + toEnd = true; + destination = new Uint8Array(0); + length = 0; + position = -1; + } else if (typeof first === 'number') { + // A size instead of a buffer: allocate it and apply the same window + // arguments Node's buffer form takes. + destination = new Uint8Array(first); + offset = second ?? 0; + length = third ?? Math.max(destination.byteLength - offset, 0); + position = fourth ?? -1; + } else if (first instanceof Uint8Array) { + destination = first; + offset = second ?? 0; + length = third ?? destination.byteLength - offset; + position = fourth ?? -1; + } else { + destination = first.buffer ?? new Uint8Array(0); + offset = first.offset ?? 0; + length = first.length ?? Math.max(destination.byteLength - offset, 0); + position = first.position ?? -1; + toEnd = first.buffer === undefined; + } + + try { + if (toEnd) { + const bytes = await this.readToEnd(position); + return { bytesRead: bytes.byteLength, buffer: bytes }; + } + // Validated before the native call, not after. The native side reads + // `length` bytes and advances the cursor, so a window that overruns the + // buffer used to consume bytes and then drop them when the result was + // trimmed to fit. Node rejects the call instead, and so must this. + const window = checkBufferRange(destination, offset, length); + const buffer = await NativeFs.read(this.fd, window.length, position); + const bytes = new Uint8Array(buffer); + const bytesRead = Math.min(bytes.byteLength, window.length); + destination.set(bytes.subarray(0, bytesRead), window.offset); + return { bytesRead, buffer: destination }; + } catch (error) { + throw withContext(error, 'read', undefined); + } + } + + /** + * Reads until a short read, which is end of file. + * + * The native `read` returns at most what it was asked for, so "the rest of the + * file" has to be assembled: a single call with a huge length would ask Rust to + * allocate that much up front. A cursor read (`position` -1) advances the + * handle's offset, so the loop continues where the previous chunk ended. + */ + private async readToEnd(position: number): Promise { + const chunks: Uint8Array[] = []; + let total = 0; + let at = position; + for (;;) { + const buffer = await NativeFs.read(this.fd, READ_CHUNK, at); + const chunk = new Uint8Array(buffer); + if (chunk.byteLength === 0) { + break; + } + chunks.push(chunk); + total += chunk.byteLength; + if (chunk.byteLength < READ_CHUNK) { + break; + } + if (at >= 0) { + at += chunk.byteLength; + } + } + const all = new Uint8Array(total); + let cursor = 0; + for (const chunk of chunks) { + all.set(chunk, cursor); + cursor += chunk.byteLength; + } + return all; + } + + async write( + data: BinaryData, + position?: number | null, + encoding?: Encoding, + ): Promise<{ bytesWritten: number; buffer: Uint8Array }>; + async write( + buffer: Uint8Array, + offset: number, + length: number, + position?: number | null, + ): Promise<{ bytesWritten: number; buffer: Uint8Array }>; + async write( + data: BinaryData, + second?: number | null, + third?: number | Encoding, + fourth?: number | null, + ): Promise<{ bytesWritten: number; buffer: Uint8Array }> { + this.assertOpen(); + let bytes: Uint8Array; + let position: number | null = null; + if (typeof data === 'string') { + // `write(string, position, encoding)`: here the number is a file position. + const encodingName = + typeof third === 'string' ? normalizeEncoding(third) : 'utf8'; + bytes = encodeString(data, encodingName ?? 'utf8'); + position = second ?? null; + } else if ( + data instanceof Uint8Array && + third !== undefined && + typeof third !== 'number' + ) { + // `write(buffer, { offset, length, position })`, which Node also accepts. + const options = third as unknown as { + offset?: number; + length?: number; + position?: number | null; + }; + const window = checkBufferRange(data, options.offset, options.length); + bytes = data.subarray(window.offset, window.offset + window.length); + position = options.position ?? null; + } else if ( + data instanceof Uint8Array && + second !== undefined && + second !== null + ) { + // `write(buffer, offset[, length[, position]])` — the numeric argument is + // into the *buffer*, not into the file. Reading it as a file position + // wrote the whole buffer at that offset instead of a slice at the cursor. + const window = checkBufferRange( + data, + second, + typeof third === 'number' ? third : undefined, + ); + bytes = data.subarray(window.offset, window.offset + window.length); + position = fourth ?? null; + } else { + bytes = toBytes(data); + position = typeof second === 'number' ? second : null; + } + try { + const bytesWritten = await NativeFs.write( + this.fd, + toArrayBuffer(bytes), + position ?? -1, + ); + return { bytesWritten, buffer: bytes }; + } catch (error) { + throw withContext(error, 'write', undefined); + } + } + + async stat(): Promise { + this.assertOpen(); + try { + return new Stats((await NativeFs.fstat(this.fd)) as StatResult); + } catch (error) { + throw withContext(error, 'fstat', undefined); + } + } + + async sync(): Promise { + this.assertOpen(); + try { + await NativeFs.fsync(this.fd); + } catch (error) { + throw withContext(error, 'fsync', undefined); + } + } + + async truncate(length?: number): Promise { + this.assertOpen(); + try { + await NativeFs.ftruncate(this.fd, length ?? 0); + } catch (error) { + throw withContext(error, 'ftruncate', undefined); + } + } + + async close(): Promise { + if (this.closed) { + return; + } + this.closed = true; + try { + await NativeFs.close(this.fd); + } catch (error) { + throw withContext(error, 'close', undefined); + } + } +} + +async function open( + path: string, + flags?: string | number | OpenOptions, + mode?: number | string, +): Promise { + const target = toPath(path); + const opts: OpenOptions = + typeof flags === 'object' && flags !== null ? flags : { flags, mode }; + const flag = flagFrom(opts.flags, constants.O_RDONLY); + try { + const fd = await NativeFs.open(target, flag, modeFrom(opts.mode)); + return new FileHandle(fd); + } catch (error) { + throw withContext(error, 'open', target); + } +} + +async function reveal(path: string): Promise { + const target = toPath(path); + try { + await NativeFs.reveal(target); + } catch (error) { + throw withContext(error, 'reveal', target); + } +} + +async function openExternal(path: string): Promise { + const target = toPath(path); + try { + await NativeFs.openExternal(target); + } catch (error) { + throw withContext(error, 'openExternal', target); + } +} + +/** + * The `fs/promises` object. + * + * The functions are declared separately and collected here so the callback layer + * in `index.ts` can wrap the same implementations rather than reimplementing + * them. + */ +export const promises = { + readFile, + writeFile, + appendFile, + readdir, + stat, + lstat, + mkdir, + rm, + rename, + copyFile, + cp, + access, + realpath, + readlink, + symlink, + truncate, + utimes, + chmod, + mkdtemp, + open, + reveal, + openExternal, +}; + +export type Promises = typeof promises; + +/** Re-exported so `fs.stat` and `fs.promises.stat` share one class identity. */ +export { Dirent, Stats }; +export type { StatResult }; diff --git a/vendor/react-native-gpui/modules/fs/src/shared.ts b/vendor/react-native-gpui/modules/fs/src/shared.ts new file mode 100644 index 0000000000..b4714eadc4 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/src/shared.ts @@ -0,0 +1,539 @@ +/** + * The pieces every face of the API needs: Node error objects, `file://` path + * conversion, the encodings Node supports, and the flag/mode translation. + * + * Nothing here touches the native module, so it stays usable from the sync, + * callback and promise layers without a cycle. + */ + +/** Bytes in any of the shapes a caller may hand a writer. */ +export type BinaryData = string | ArrayBuffer | ArrayBufferView; + +/** The encodings this module implements, after aliasing. */ +export type NormalizedEncoding = + 'utf8' | 'utf16le' | 'latin1' | 'base64' | 'hex'; + +/** The encodings accepted in an options object, with Node's aliases. */ +export type Encoding = + | 'utf8' + | 'utf-8' + | 'utf16le' + | 'utf-16le' + | 'ucs2' + | 'ucs-2' + | 'latin1' + | 'binary' + | 'ascii' + | 'base64' + | 'hex'; + +/** What a rejected call looks like: Node's `ErrnoException`. */ +export interface NodeError extends Error { + code?: string; + errno?: number; + syscall?: string; + path?: string; + /** The second path of a two-path call, as Node names it on `rename` and `cp`. */ + dest?: string; +} + +/** Builds an error with a code but no OS call behind it. */ +export function nodeError(code: string, message: string): NodeError { + const error = new Error(message) as NodeError; + error.code = code; + return error; +} + +/** The error Node raises when a path is not there, in its message format. */ +export function missingPath(path: string, syscall: string): NodeError { + return nodeError( + 'ENOENT', + `ENOENT: no such file or directory, ${syscall} '${path}'`, + ); +} + +/** + * Adds the fields only the JS side knows to an error a native call rejected + * with. + * + * The Rust layer reports `code` and a Node-format message, but it deliberately + * does not guess `syscall` — one native `stat` serves `stat` and `lstat`, and it + * cannot know the spelling of the path the caller passed. + */ +export function withContext( + error: unknown, + syscall: string, + path?: string, +): NodeError { + const wrapped: NodeError = + error instanceof Error + ? (error as NodeError) + : nodeError('EIO', String(error)); + if (wrapped.syscall === undefined) { + wrapped.syscall = syscall; + } + if (path !== undefined && wrapped.path === undefined) { + wrapped.path = path; + } + return wrapped; +} + +/** + * `file://` URLs to paths, leaving plain paths alone. + * + * Node accepts `URL` objects and `file:` strings interchangeably; this API takes + * strings, so the conversion is textual. Percent escapes are decoded because a + * path with a space arrives as `file:///a%20b`. + */ +export function toPath(path: string): string { + if (!path.startsWith('file://')) { + return path; + } + let rest = path.slice('file://'.length); + // An empty or `localhost` authority is the local machine. + if (rest.startsWith('localhost/')) { + rest = rest.slice('localhost'.length); + } + const decoded = decodeURIComponent(rest); + // `file:///C:/x` is a drive path on Windows, not a path under `/`. + if (/^\/[A-Za-z]:[\\/]/.test(decoded)) { + return decoded.slice(1); + } + return decoded; +} + +/** + * Node's bounds check for a buffer window, as `RangeError` with + * `code = 'ERR_OUT_OF_RANGE'`. + * + * Both `read` and `write` take a window into a caller's buffer, and the native + * side reads or writes `length` bytes before the result is trimmed to fit — so an + * oversized `length` would consume bytes from the file and silently drop them + * rather than failing. Node rejects the call instead, and its wording is + * reproduced because callers match on it: + * + * The value of "length" is out of range. It must be <= 2. Received 8 + */ +export function checkBufferRange( + buffer: Uint8Array, + offset: number | undefined, + length: number | undefined, +): { offset: number; length: number } { + const start = offset ?? 0; + if (!Number.isInteger(start) || start < 0 || start > buffer.byteLength) { + throw nodeError( + 'ERR_OUT_OF_RANGE', + `The value of "offset" is out of range. It must be >= 0 && <= ${buffer.byteLength}. Received ${start}`, + ); + } + const count = length ?? buffer.byteLength - start; + const available = buffer.byteLength - start; + if (!Number.isInteger(count) || count < 0 || count > available) { + throw nodeError( + 'ERR_OUT_OF_RANGE', + `The value of "length" is out of range. It must be <= ${available}. Received ${count}`, + ); + } + return { offset: start, length: count }; +} + +/** + * A recursive `readdir` entry as the path Node reports. + * + * Node's `readdir(dir, { recursive: true })` returns paths relative to `dir` + * — `nested/deep/leaf.txt`, not `leaf.txt` — so a caller can join them onto the + * root and get a real path. The native side reports each entry's absolute + * containing directory, so the relative name is what remains after removing the + * root. A non-recursive listing names the direct children only, which is already + * what `entry.name` holds, so this is used for the recursive case alone. + * + * The separator is taken from the paths themselves rather than from a platform + * check: the native side returns native separators, and a module that reaches + * for `process.platform` needs Node's types for a question the data already + * answers. + */ +export function relativeEntryName( + root: string, + parentPath: string, + name: string, +): string { + const separator = root.includes('\\') ? '\\' : '/'; + const split = (value: string) => value.split(/[\\/]+/u).filter(Boolean); + const rootParts = split(root); + const parentParts = split(parentPath); + // Compare from the front. A parent that does not start with the root means the + // entry is somewhere the caller did not ask about, which should not happen, so + // the bare name is a safer answer than a fabricated path. + const inside = rootParts.every((part, index) => parentParts[index] === part); + if (!inside) { + return name; + } + return [...parentParts.slice(rootParts.length), name].join(separator); +} + +/** The encoding name, folded to the set implemented here. */ +export function normalizeEncoding( + encoding: string | null | undefined, +): NormalizedEncoding | null { + if (encoding === null || encoding === undefined) { + return null; + } + switch (encoding.toLowerCase()) { + case 'utf8': + case 'utf-8': + return 'utf8'; + case 'utf16le': + case 'utf-16le': + case 'ucs2': + case 'ucs-2': + return 'utf16le'; + case 'latin1': + case 'binary': + case 'ascii': + return 'latin1'; + case 'base64': + return 'base64'; + case 'hex': + return 'hex'; + default: + throw nodeError( + 'ERR_INVALID_ARG_VALUE', + `The argument 'encoding' is invalid encoding. Received '${encoding}'`, + ); + } +} + +const BASE64_ALPHABET = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + +const BASE64_VALUES: Record = (() => { + const values: Record = {}; + for (let index = 0; index < BASE64_ALPHABET.length; index += 1) { + values[BASE64_ALPHABET[index]] = index; + } + // The URL-safe alphabet is accepted too: `Buffer` decodes either. + values['-'] = 62; + values._ = 63; + return values; +})(); + +function encodeBase64(bytes: Uint8Array): string { + let out = ''; + for (let index = 0; index < bytes.byteLength; index += 3) { + const first = bytes[index]; + const second = index + 1 < bytes.byteLength ? bytes[index + 1] : 0; + const third = index + 2 < bytes.byteLength ? bytes[index + 2] : 0; + const triple = (first << 16) | (second << 8) | third; + out += BASE64_ALPHABET[(triple >> 18) & 0x3f]; + out += BASE64_ALPHABET[(triple >> 12) & 0x3f]; + out += + index + 1 < bytes.byteLength + ? BASE64_ALPHABET[(triple >> 6) & 0x3f] + : '='; + out += index + 2 < bytes.byteLength ? BASE64_ALPHABET[triple & 0x3f] : '='; + } + return out; +} + +function decodeBase64(text: string): Uint8Array { + const bytes: number[] = []; + let pending = 0; + let bits = 0; + for (const character of text) { + if ( + character === '=' || + character === '\n' || + character === '\r' || + character === ' ' + ) { + continue; + } + const value = BASE64_VALUES[character]; + if (value === undefined) { + // Node stops at the first character that is not base64 rather than + // failing, so `Buffer.from('abc$', 'base64')` decodes the prefix. + break; + } + pending = (pending << 6) | value; + bits += 6; + if (bits >= 8) { + bits -= 8; + bytes.push((pending >> bits) & 0xff); + } + } + return Uint8Array.from(bytes); +} + +function encodeHex(bytes: Uint8Array): string { + let out = ''; + for (let index = 0; index < bytes.byteLength; index += 1) { + out += bytes[index].toString(16).padStart(2, '0'); + } + return out; +} + +function decodeHex(text: string): Uint8Array { + const bytes: number[] = []; + for (let index = 0; index + 1 < text.length; index += 2) { + const value = Number.parseInt(text.slice(index, index + 2), 16); + if (Number.isNaN(value)) { + break; + } + bytes.push(value); + } + return Uint8Array.from(bytes); +} + +function encodeUtf8(text: string): Uint8Array { + const bytes: number[] = []; + for (let index = 0; index < text.length; index += 1) { + let code = text.charCodeAt(index); + // A surrogate pair is one code point; without this an emoji becomes two + // sequences of replacement characters. + if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length) { + const next = text.charCodeAt(index + 1); + if (next >= 0xdc00 && next <= 0xdfff) { + code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00); + index += 1; + } + } + if (code < 0x80) { + bytes.push(code); + } else if (code < 0x800) { + bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); + } else if (code < 0x10000) { + bytes.push( + 0xe0 | (code >> 12), + 0x80 | ((code >> 6) & 0x3f), + 0x80 | (code & 0x3f), + ); + } else { + bytes.push( + 0xf0 | (code >> 18), + 0x80 | ((code >> 12) & 0x3f), + 0x80 | ((code >> 6) & 0x3f), + 0x80 | (code & 0x3f), + ); + } + } + return Uint8Array.from(bytes); +} + +function decodeUtf8(bytes: Uint8Array): string { + let out = ''; + let index = 0; + while (index < bytes.byteLength) { + const first = bytes[index]; + const width = + first < 0x80 + ? 1 + : first >= 0xf0 + ? 4 + : first >= 0xe0 + ? 3 + : first >= 0xc0 + ? 2 + : 0; + if (width === 0 || index + width > bytes.byteLength) { + // Node decodes malformed input with U+FFFD instead of throwing, so a + // file that is not valid UTF-8 still reads as text. + out += '\ufffd'; + index += 1; + continue; + } + let code = width === 1 ? first : first & (0xff >> (width + 1)); + let valid = true; + for (let offset = 1; offset < width; offset += 1) { + const continuation = bytes[index + offset]; + if ((continuation & 0xc0) !== 0x80) { + valid = false; + break; + } + code = (code << 6) | (continuation & 0x3f); + } + if (!valid) { + out += '\ufffd'; + index += 1; + continue; + } + out += String.fromCodePoint(code); + index += width; + } + return out; +} + +function encodeUtf16le(text: string): Uint8Array { + const bytes = new Uint8Array(text.length * 2); + for (let index = 0; index < text.length; index += 1) { + const code = text.charCodeAt(index); + bytes[index * 2] = code & 0xff; + bytes[index * 2 + 1] = code >> 8; + } + return bytes; +} + +function decodeUtf16le(bytes: Uint8Array): string { + const codes: number[] = []; + for (let index = 0; index + 1 < bytes.byteLength; index += 2) { + codes.push(bytes[index] | (bytes[index + 1] << 8)); + } + return chunkedFromCharCode(codes); +} + +function chunkedFromCharCode(codes: number[]): string { + let out = ''; + // `apply` has an argument-count limit, so long strings are built in pieces. + for (let index = 0; index < codes.length; index += 8192) { + out += String.fromCharCode.apply(null, codes.slice(index, index + 8192)); + } + return out; +} + +/** Text to bytes for the encodings implemented in JS. */ +export function encodeString( + text: string, + encoding: NormalizedEncoding, +): Uint8Array { + switch (encoding) { + case 'utf8': + return encodeUtf8(text); + case 'utf16le': + return encodeUtf16le(text); + case 'latin1': { + const bytes = new Uint8Array(text.length); + for (let index = 0; index < text.length; index += 1) { + bytes[index] = text.charCodeAt(index) & 0xff; + } + return bytes; + } + case 'base64': + return decodeBase64(text); + case 'hex': + return decodeHex(text); + } +} + +/** Bytes to text for the encodings implemented in JS. */ +export function decodeString( + bytes: Uint8Array, + encoding: NormalizedEncoding, +): string { + switch (encoding) { + case 'utf8': + return decodeUtf8(bytes); + case 'utf16le': + return decodeUtf16le(bytes); + case 'latin1': { + const codes: number[] = []; + for (let index = 0; index < bytes.byteLength; index += 1) { + codes.push(bytes[index]); + } + return chunkedFromCharCode(codes); + } + case 'base64': + return encodeBase64(bytes); + case 'hex': + return encodeHex(bytes); + } +} + +/** + * Normalises any accepted data shape to an `ArrayBuffer` the bridge can send. + * + * A `Uint8Array` may be a window onto a larger allocation; slicing to the view's + * own range is what stops a writer from sending bytes that belong to something + * else in the same buffer. + */ +export function toArrayBuffer( + data: BinaryData, + encoding: NormalizedEncoding = 'utf8', +): ArrayBuffer { + if (typeof data === 'string') { + const bytes = encodeString(data, encoding); + return bytes.buffer.slice( + bytes.byteOffset, + bytes.byteOffset + bytes.byteLength, + ) as ArrayBuffer; + } + if (data instanceof ArrayBuffer) { + return data; + } + const view = data as ArrayBufferView; + const buffer = view.buffer as ArrayBuffer; + if (view.byteOffset === 0 && view.byteLength === buffer.byteLength) { + return buffer; + } + return buffer.slice(view.byteOffset, view.byteOffset + view.byteLength); +} + +/** A byte view of `toArrayBuffer`'s result, without copying. */ +export function toBytes( + data: BinaryData, + encoding: NormalizedEncoding = 'utf8', +): Uint8Array { + return new Uint8Array(toArrayBuffer(data, encoding)); +} + +/** Node's numeric `open` flags, by their string spelling. */ +const FLAG_STRINGS: Record = { + r: 0, + 'r+': 2, + rs: 0, + sr: 0, + w: 1 | 64 | 512, + wx: 1 | 64 | 512 | 128, + xw: 1 | 64 | 512 | 128, + 'w+': 2 | 64 | 512, + 'wx+': 2 | 64 | 512 | 128, + 'xw+': 2 | 64 | 512 | 128, + a: 1 | 64 | 1024, + ax: 1 | 64 | 1024 | 128, + xa: 1 | 64 | 1024 | 128, + 'a+': 2 | 64 | 1024, + 'ax+': 2 | 64 | 1024 | 128, + 'xa+': 2 | 64 | 1024 | 128, + as: 1 | 64 | 1024, + sa: 1 | 64 | 1024, +}; + +/** A `flag` option as the number the native side decodes. */ +export function flagFrom( + flag: string | number | undefined, + fallback: number, +): number { + if (flag === undefined) { + return fallback; + } + if (typeof flag === 'number') { + return flag; + } + const value = FLAG_STRINGS[flag]; + if (value === undefined) { + throw nodeError( + 'ERR_INVALID_ARG_VALUE', + `The argument 'flag' is invalid. Received '${flag}'`, + ); + } + return value; +} + +/** A `mode` option as a number. A string is octal, as in Node. */ +export function modeFrom( + mode: number | string | undefined, + fallback = 0o666, +): number { + if (mode === undefined) { + return fallback; + } + if (typeof mode === 'number') { + return mode; + } + const parsed = Number.parseInt(mode, 8); + if (Number.isNaN(parsed)) { + throw nodeError( + 'ERR_INVALID_ARG_VALUE', + `The argument 'mode' is invalid. Received '${mode}'`, + ); + } + return parsed; +} diff --git a/vendor/react-native-gpui/modules/fs/src/stats.ts b/vendor/react-native-gpui/modules/fs/src/stats.ts new file mode 100644 index 0000000000..1345af70cc --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/src/stats.ts @@ -0,0 +1,196 @@ +/** + * `Stats` and `Dirent`, built over the plain objects the native module returns. + * + * The module crosses the bridge with numbers rather than methods — a generated + * TurboModule result cannot carry predicates — so the classes live here and + * reproduce the shape Node's callers test for. The `kind` field is the coarse + * classification the native side can make without a platform syscall; `mode` is + * there for the finer predicates, and is zero on Windows where there are no + * Unix mode bits at all. + */ + +import { constants } from './constants'; + +/** The native `FileKind` numbers; must match `module::kind` in Rust. */ +export const FILE_KIND = { + file: 0, + directory: 1, + symlink: 2, + other: 3, +} as const; + +/** The shape `stat`, `lstat` and `fstat` return across the bridge. */ +export interface StatResult { + size: number; + mode: number; + kind: number; + atimeMs: number; + mtimeMs: number; + ctimeMs: number; + birthtimeMs: number; + dev: number; + ino: number; + nlink: number; + uid: number; + gid: number; +} + +/** The shape `readdir` returns across the bridge. */ +export interface DirEntryResult { + name: string; + kind: number; + parentPath: string; +} + +/** The type bits of a mode, or 0 when the platform does not report them. */ +function fileTypeBits(mode: number): number { + return mode & constants.S_IFMT; +} + +/** + * Node's `fs.Stats`. + * + * The fields are plain properties rather than getters: they are copied once from + * the native result, and a caller destructuring the object sees the same values + * Node's does. + */ +export class Stats { + readonly dev: number; + readonly ino: number; + readonly mode: number; + readonly nlink: number; + readonly uid: number; + readonly gid: number; + readonly rdev: number; + readonly size: number; + readonly blksize: number; + readonly blocks: number; + + readonly atimeMs: number; + readonly mtimeMs: number; + readonly ctimeMs: number; + readonly birthtimeMs: number; + + readonly atime: Date; + readonly mtime: Date; + readonly ctime: Date; + readonly birthtime: Date; + + /** The native classification, kept because it answers predicates the mode + * cannot on platforms that report no type bits. */ + private readonly kind: number; + + constructor(result: StatResult) { + this.dev = result.dev; + this.ino = result.ino; + this.mode = result.mode; + this.nlink = result.nlink; + this.uid = result.uid; + this.gid = result.gid; + // The native result has no `rdev`, block size or block count; Node reports + // zero for them where the platform cannot say, so zero is the honest value. + this.rdev = 0; + this.size = result.size; + this.blksize = 0; + this.blocks = 0; + + this.atimeMs = result.atimeMs; + this.mtimeMs = result.mtimeMs; + this.ctimeMs = result.ctimeMs; + this.birthtimeMs = result.birthtimeMs; + + this.atime = new Date(result.atimeMs); + this.mtime = new Date(result.mtimeMs); + this.ctime = new Date(result.ctimeMs); + this.birthtime = new Date(result.birthtimeMs); + + this.kind = result.kind; + } + + isFile(): boolean { + const bits = fileTypeBits(this.mode); + return bits === 0 + ? this.kind === FILE_KIND.file + : bits === constants.S_IFREG; + } + + isDirectory(): boolean { + const bits = fileTypeBits(this.mode); + return bits === 0 + ? this.kind === FILE_KIND.directory + : bits === constants.S_IFDIR; + } + + isSymbolicLink(): boolean { + const bits = fileTypeBits(this.mode); + return bits === 0 + ? this.kind === FILE_KIND.symlink + : bits === constants.S_IFLNK; + } + + isBlockDevice(): boolean { + return fileTypeBits(this.mode) === constants.S_IFBLK; + } + + isCharacterDevice(): boolean { + return fileTypeBits(this.mode) === constants.S_IFCHR; + } + + isFIFO(): boolean { + return fileTypeBits(this.mode) === constants.S_IFIFO; + } + + isSocket(): boolean { + return fileTypeBits(this.mode) === constants.S_IFSOCK; + } +} + +/** + * Node's `fs.Dirent`. + * + * `parentPath` is `''` when the caller already held the parent, matching what + * `readdir` with `{withFileTypes: true}` produces there. + */ +export class Dirent { + readonly name: string; + readonly parentPath: string; + readonly path: string; + + private readonly kind: number; + + constructor(result: DirEntryResult) { + this.name = result.name; + this.parentPath = result.parentPath; + // `path` is the older name for the same field, and libraries still read it. + this.path = result.parentPath; + this.kind = result.kind; + } + + isFile(): boolean { + return this.kind === FILE_KIND.file; + } + + isDirectory(): boolean { + return this.kind === FILE_KIND.directory; + } + + isSymbolicLink(): boolean { + return this.kind === FILE_KIND.symlink; + } + + isBlockDevice(): boolean { + return false; + } + + isCharacterDevice(): boolean { + return false; + } + + isFIFO(): boolean { + return false; + } + + isSocket(): boolean { + return false; + } +} diff --git a/vendor/react-native-gpui/modules/fs/src/watch.ts b/vendor/react-native-gpui/modules/fs/src/watch.ts new file mode 100644 index 0000000000..7bc3b5a277 --- /dev/null +++ b/vendor/react-native-gpui/modules/fs/src/watch.ts @@ -0,0 +1,141 @@ +/** + * `watch`: the JS half of the typed watcher event. + * + * The native module returns a watcher id; every event it produces carries that + * id, so one shared emitter serves every open watcher and each listener keeps + * only its own. The subscription is taken through the generated typed emitter on + * the module (`NativeFs.onWatchEvent`), which is the same mechanism the fixture's + * `onProgress` uses in `RustTurboModuleFixture.tsx`, rather than through + * `NativeEventEmitter`: the spec declares `onWatchEvent` as a typed + * `CodegenTypes.EventEmitter`, and the bridge installs a listener for exactly + * that; `NativeEventEmitter` would subscribe on the global device-emitter channel + * that only `sendEvent` writes to, so it would never fire. + */ + +import NativeFs, { type WatchEvent } from './NativeFs'; +import { toPath, withContext, type NodeError } from './shared'; + +/** The event names Node uses for the two things a watcher reports. */ +export type WatchEventType = 'rename' | 'change'; + +export interface WatchOptions { + recursive?: boolean; + encoding?: string; +} + +export interface FSWatcher { + close(): Promise; +} + +export type WatchListener = ( + eventType: WatchEventType, + filename: string | null, +) => void; + +/** + * Which of Node's two event types a native kind is. + * + * Node folds everything it sees into `rename` (the entry appeared, vanished or + * moved) and `change` (its contents or metadata changed). The caller's callback + * signature is Node's, so the mapping happens here while the native kind stays + * available to anyone who subscribes to the module directly. + */ +function eventTypeOf(kind: string): WatchEventType { + return kind === 'modify' ? 'change' : 'rename'; +} + +/** The file a path names, relative to the watched root where it is below it. */ +/** + * The filename a listener is given. + * + * The native side already reported it relative to the watch root, and its own + * name for a single-file watch: it canonicalises the root, which is where macOS + * decides between `/var` and `/private/var`, and it strips whichever spelling the + * event actually carries. Stripping a prefix here as well would be a second, + * weaker guess at the same question — a relative root never matches, and Windows + * separates with `\` — so the event path is passed through unchanged. + */ +function fileNameOf(eventPath: string): string { + return eventPath; +} + +/** + * Starts a watcher. + * + * The id arrives asynchronously, but the caller expects a handle it can close + * immediately, so the handle is created first and closing it before the id is + * known sets a flag the resolution honours. That is the one ordering hazard in + * this API, and it is invisible from the outside. + * + * Node reports a failure to start through an `error` event on the watcher. This + * handle has no event surface, so the failure is held and rejected from + * `close()` instead: it can then never become an unhandled rejection, and a + * caller that closes what it opened is guaranteed to see it. + */ +export function watch(path: string, listener: WatchListener): FSWatcher; +export function watch( + path: string, + options: WatchOptions | null, + listener: WatchListener, +): FSWatcher; +export function watch( + path: string, + optionsOrListener: WatchOptions | null | WatchListener, + maybeListener?: WatchListener, +): FSWatcher { + const options = + typeof optionsOrListener === 'function' ? {} : (optionsOrListener ?? {}); + const listener = + typeof optionsOrListener === 'function' ? optionsOrListener : maybeListener; + if (typeof listener !== 'function') { + throw new TypeError('watch requires a listener function'); + } + + const target = toPath(path); + let id: number | null = null; + let closed = false; + let failure: NodeError | null = null; + let subscription: { remove(): void } | null = null; + + const ready = NativeFs.watch(target, options.recursive ?? false) + .then(watcherId => { + id = watcherId; + if (closed) { + // `close()` ran while the id was in flight; stop the native watcher now + // that there is something to stop. + NativeFs.unwatch(watcherId); + return; + } + subscription = NativeFs.onWatchEvent((event: WatchEvent) => { + if (event.id !== id) { + return; + } + // The encoding option only affects how a non-ASCII name is decoded, and + // every name already arrives as a JS string. + listener(eventTypeOf(event.kind), fileNameOf(event.path)); + }); + }) + .catch((error: unknown) => { + failure = withContext(error, 'watch', target); + }); + + return { + async close(): Promise { + closed = true; + subscription?.remove(); + subscription = null; + if (id !== null) { + NativeFs.unwatch(id); + id = null; + } + // Awaiting the start makes `close()` immediately after `watch()` still stop + // the native watcher rather than racing it. + await ready; + if (failure !== null) { + const error = failure; + failure = null; + throw error; + } + }, + }; +} diff --git a/vendor/react-native-gpui/modules/gesture-handler/desktop/CMakeLists.txt b/vendor/react-native-gpui/modules/gesture-handler/desktop/CMakeLists.txt new file mode 100644 index 0000000000..5a14af4856 --- /dev/null +++ b/vendor/react-native-gpui/modules/gesture-handler/desktop/CMakeLists.txt @@ -0,0 +1,19 @@ +if(NOT RN_GPUI_PROJECT_ROOT) + message(FATAL_ERROR + "@react-native-gpui/gesture-handler was added by the platform; RN_GPUI_PROJECT_ROOT must be set by its build.rs") +endif() +rn_gpui_find_node_module(react-native-gesture-handler _gesture_root) +if(NOT EXISTS "${_gesture_root}/src/specs/NativeRNGestureHandlerModule.ts") + message(FATAL_ERROR "react-native-gesture-handler 3.3.x was not found at ${_gesture_root}") +endif() + +file(READ "${_gesture_root}/package.json" _gesture_package_json) +string(JSON _gesture_version GET "${_gesture_package_json}" version) +if(NOT _gesture_version MATCHES "^3\\.3\\.") + message(FATAL_ERROR "@react-native-gpui/gesture-handler supports react-native-gesture-handler 3.3.x, found ${_gesture_version}") +endif() + +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/cpp/GPUIGestureHandlerModule.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS + "${RN_GPUI_SYS_CPP_DIR}") diff --git a/vendor/react-native-gpui/modules/gesture-handler/desktop/cpp/GPUIGestureHandlerModule.cpp b/vendor/react-native-gpui/modules/gesture-handler/desktop/cpp/GPUIGestureHandlerModule.cpp new file mode 100644 index 0000000000..9da87b95d6 --- /dev/null +++ b/vendor/react-native-gpui/modules/gesture-handler/desktop/cpp/GPUIGestureHandlerModule.cpp @@ -0,0 +1,500 @@ +#include "GpuiHost.h" +#include "GpuiModules.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +using facebook::jsi::Runtime; +using facebook::react::TurboModule; +using rngpui::extension::RawPointerEvent; +using rngpui::extension::RecognizedGesture; + +constexpr int32_t kUndetermined = 0; +constexpr int32_t kFailed = 1; +constexpr int32_t kBegan = 2; +constexpr int32_t kCancelled = 3; +constexpr int32_t kActive = 4; +constexpr int32_t kEnd = 5; + +struct Handler { + std::string name; + folly::dynamic config{folly::dynamic::object}; + int32_t viewTag{-1}; + int32_t actionType{0}; + int32_t state{kUndetermined}; + float startX{0}; + float startY{0}; + float lastX{0}; + float lastY{0}; + double startTime{0}; + double lastTime{0}; + bool recognized{false}; + std::vector simultaneousHandlers; + std::vector waitFor; + std::vector blocksHandlers; +}; + +std::mutex handlersMutex; +std::unordered_map handlers; + +folly::dynamic configObject(folly::dynamic config) { + return config.isObject() ? std::move(config) : folly::dynamic::object(); +} + +float number(const folly::dynamic& object, const char* key, float fallback) { + if (!object.isObject() || !object.get_ptr(key)) + return fallback; + const auto& value = object[key]; + if (value.isNumber()) + return static_cast(value.asDouble()); + return fallback; +} + +bool enabled(const Handler& handler) { + const auto* value = handler.config.get_ptr("enabled"); + return value == nullptr || !value->isBool() || value->asBool(); +} + +bool isPan(const Handler& handler) { + return handler.name == "Pan" || handler.name == "PanGestureHandler"; +} + +bool isTap(const Handler& handler) { + return handler.name == "Tap" || handler.name == "TapGestureHandler"; +} + +bool isLongPress(const Handler& handler) { + return handler.name == "LongPress" || + handler.name == "LongPressGestureHandler"; +} + +bool gestureTracingEnabled() { + return std::getenv("RN_GPUI_DEBUG_GESTURES") != nullptr; +} + +folly::dynamic payloadFor( + int32_t handlerTag, + const Handler& handler, + int32_t state, + int32_t oldState, + const RawPointerEvent& pointer, + bool stateChange) { + const float dx = pointer.x - handler.startX; + const float dy = pointer.y - handler.startY; + const double elapsed = std::max(1.0, pointer.timestamp - handler.lastTime); + folly::dynamic payload = folly::dynamic::object("handlerTag", handlerTag)( + "state", state)("x", pointer.x - handler.startX)( + "y", pointer.y - handler.startY)("absoluteX", pointer.x)( + "absoluteY", pointer.y)("translationX", dx)("translationY", dy)( + "velocityX", (pointer.x - handler.lastX) * 1000.0 / elapsed)( + "velocityY", (pointer.y - handler.lastY) * 1000.0 / elapsed)( + "numberOfPointers", 1)("pointerType", 2)( + "duration", pointer.timestamp - handler.startTime); + if (stateChange) { + payload["oldState"] = oldState; + } + return payload; +} + +void appendEvent( + std::vector& events, + int32_t viewTag, + const char* type, + const folly::dynamic& payload) { + events.push_back({viewTag, type, folly::toJson(payload)}); +} + +void appendState( + std::vector& events, + int32_t handlerTag, + Handler& handler, + int32_t nextState, + const RawPointerEvent& pointer) { + if (handler.state == nextState) + return; + const auto oldState = handler.state; + handler.state = nextState; + if (nextState == kActive) { + handler.recognized = true; + } + appendEvent( + events, + handler.viewTag, + "topGestureHandlerStateChange", + payloadFor(handlerTag, handler, nextState, oldState, pointer, true)); +} + +std::vector relationTags( + const folly::dynamic& relations, + const char* key) { + std::vector tags; + const auto* values = relations.get_ptr(key); + if (values == nullptr || !values->isArray()) { + return tags; + } + tags.reserve(values->size()); + for (const auto& value : *values) { + if (value.isInt()) { + tags.push_back(static_cast(value.asInt())); + } + } + return tags; +} + +bool hasTag(const std::vector& tags, int32_t tag) { + return std::find(tags.begin(), tags.end(), tag) != tags.end(); +} + +bool hasRecognizedWaitFor(const Handler& handler) { + for (const auto tag : handler.waitFor) { + const auto dependency = handlers.find(tag); + if (dependency != handlers.end() && dependency->second.recognized) { + return true; + } + } + return false; +} + +bool hasUnresolvedWaitFor(const Handler& handler) { + for (const auto tag : handler.waitFor) { + const auto dependency = handlers.find(tag); + if (dependency != handlers.end() && dependency->second.state == kBegan && + !dependency->second.recognized) { + return true; + } + } + return false; +} + +bool blockedByActiveHandler(int32_t handlerTag) { + for (const auto& [otherTag, other] : handlers) { + if (otherTag != handlerTag && other.state == kActive && + hasTag(other.blocksHandlers, handlerTag)) { + return true; + } + } + return false; +} + +std::vector processPointer(const RawPointerEvent& pointer) { + std::vector events; + std::lock_guard lock(handlersMutex); + if (gestureTracingEnabled() && pointer.phase != 4) { + LOG(INFO) << "[gesture-handler] pointer phase=" << pointer.phase << " at (" + << pointer.x << ',' << pointer.y + << ") chain=" << pointer.targetChain.size() << " tags"; + } + if (pointer.phase == 0) { + for (auto& [_, handler] : handlers) { + if (!enabled(handler) || handler.viewTag < 0 || + std::find( + pointer.targetChain.begin(), + pointer.targetChain.end(), + handler.viewTag) == pointer.targetChain.end()) { + continue; + } + handler.state = kUndetermined; + handler.startX = pointer.x; + handler.startY = pointer.y; + handler.lastX = pointer.x; + handler.lastY = pointer.y; + handler.startTime = pointer.timestamp; + handler.lastTime = pointer.timestamp; + handler.recognized = false; + appendState(events, _, handler, kBegan, pointer); + if (gestureTracingEnabled()) { + LOG(INFO) << "[gesture-handler] began handler=" << _ + << " name=" << handler.name << " view=" << handler.viewTag; + } + } + return events; + } + + if (pointer.phase == 2) { + // Resolve recognizers that cannot succeed on release before testing tap + // handlers that may be waiting for them to fail. + for (auto& [handlerTag, handler] : handlers) { + if (handler.state != kBegan || isTap(handler)) { + continue; + } + const float dx = pointer.x - handler.startX; + const float dy = pointer.y - handler.startY; + if (isLongPress(handler)) { + const auto elapsed = pointer.timestamp - handler.startTime; + const auto minDuration = + number(handler.config, "minDurationMs", 500.0f); + if (elapsed >= minDuration && + std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f) && + !hasRecognizedWaitFor(handler) && + !blockedByActiveHandler(handlerTag)) { + appendState(events, handlerTag, handler, kActive, pointer); + } else { + appendState(events, handlerTag, handler, kFailed, pointer); + } + } else { + appendState(events, handlerTag, handler, kFailed, pointer); + } + } + } + + for (auto& [handlerTag, handler] : handlers) { + if (handler.state != kBegan && handler.state != kActive) + continue; + if (pointer.phase == 4) { + const float dx = pointer.x - handler.startX; + const float dy = pointer.y - handler.startY; + const auto elapsed = pointer.timestamp - handler.startTime; + const auto minDuration = number(handler.config, "minDurationMs", 500.0f); + if (gestureTracingEnabled() && handler.state == kBegan && + isLongPress(handler) && elapsed >= minDuration) { + LOG(INFO) << "[gesture-handler] long-press threshold handler=" + << handlerTag << " elapsed=" << elapsed + << " required=" << minDuration + << " distance=" << std::hypot(dx, dy); + } + if (handler.state == kBegan && isLongPress(handler) && + elapsed >= minDuration && + std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f)) { + if (hasRecognizedWaitFor(handler) || + blockedByActiveHandler(handlerTag)) { + appendState(events, handlerTag, handler, kFailed, pointer); + } else if (!hasUnresolvedWaitFor(handler)) { + appendState(events, handlerTag, handler, kActive, pointer); + } + } + } else if (pointer.phase == 1) { + const float dx = pointer.x - handler.startX; + const float dy = pointer.y - handler.startY; + if (handler.state == kBegan && isPan(handler) && + std::hypot(dx, dy) >= number(handler.config, "minDist", 3.0f)) { + if (hasRecognizedWaitFor(handler) || + blockedByActiveHandler(handlerTag)) { + appendState(events, handlerTag, handler, kFailed, pointer); + } else if (!hasUnresolvedWaitFor(handler)) { + appendState(events, handlerTag, handler, kActive, pointer); + } + } else if ( + handler.state == kBegan && isLongPress(handler) && + pointer.timestamp - handler.startTime >= + number(handler.config, "minDurationMs", 500.0f) && + std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f)) { + if (hasRecognizedWaitFor(handler) || + blockedByActiveHandler(handlerTag)) { + appendState(events, handlerTag, handler, kFailed, pointer); + } else if (!hasUnresolvedWaitFor(handler)) { + appendState(events, handlerTag, handler, kActive, pointer); + } + } + if (handler.state == kActive && + (isPan(handler) || isLongPress(handler))) { + appendEvent( + events, + handler.viewTag, + "topGestureHandlerEvent", + payloadFor( + handlerTag, + handler, + handler.state, + kUndetermined, + pointer, + false)); + } + } else if (pointer.phase == 2) { + const float dx = pointer.x - handler.startX; + const float dy = pointer.y - handler.startY; + if (isTap(handler) && handler.state == kBegan && + pointer.timestamp - handler.startTime <= + number(handler.config, "maxDurationMs", 500.0f) && + std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f)) { + if (hasRecognizedWaitFor(handler) || + blockedByActiveHandler(handlerTag)) { + appendState(events, handlerTag, handler, kFailed, pointer); + } else if (!hasUnresolvedWaitFor(handler)) { + appendState(events, handlerTag, handler, kActive, pointer); + } else { + appendState(events, handlerTag, handler, kFailed, pointer); + } + } else if ( + isLongPress(handler) && handler.state == kBegan && + pointer.timestamp - handler.startTime >= + number(handler.config, "minDurationMs", 500.0f) && + std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f)) { + appendState(events, handlerTag, handler, kActive, pointer); + } else if (handler.state == kBegan) { + appendState(events, handlerTag, handler, kFailed, pointer); + } + if (handler.state == kActive) { + if (isTap(handler) || isLongPress(handler)) { + appendEvent( + events, + handler.viewTag, + "topGestureHandlerEvent", + payloadFor( + handlerTag, + handler, + handler.state, + kUndetermined, + pointer, + false)); + } + appendState(events, handlerTag, handler, kEnd, pointer); + } + handler.lastX = pointer.x; + handler.lastY = pointer.y; + handler.lastTime = pointer.timestamp; + } else if (pointer.phase == 3) { + appendState( + events, + handlerTag, + handler, + handler.state == kActive ? kCancelled : kFailed, + pointer); + } + } + if (gestureTracingEnabled() && (pointer.phase != 4 || !events.empty())) { + LOG(INFO) << "[gesture-handler] recognized events=" << events.size(); + } + return events; +} + +class GPUIGestureHandlerModule final + : public facebook::react::NativeRNGestureHandlerModuleSpec< + GPUIGestureHandlerModule> { + public: + explicit GPUIGestureHandlerModule( + std::shared_ptr jsInvoker) + : NativeRNGestureHandlerModuleSpec(std::move(jsInvoker)) {} + + // The JS side queues operations and flushes them in order, so every method + // has to observe the same order the JS thread produced. + static constexpr rngpui::ModuleThread kThread = rngpui::ModuleThread::JS; + + void createGestureHandler( + Runtime&, + std::string handlerName, + double handlerTag, + folly::dynamic config) { + Handler handler; + handler.name = std::move(handlerName); + handler.config = configObject(std::move(config)); + std::lock_guard lock(handlersMutex); + handlers.insert_or_assign( + static_cast(handlerTag), std::move(handler)); + } + + void attachGestureHandler( + Runtime&, + double handlerTag, + double viewTag, + double actionType) { + const auto tag = static_cast(handlerTag); + std::lock_guard lock(handlersMutex); + auto found = handlers.find(tag); + if (found != handlers.end()) { + found->second.viewTag = static_cast(viewTag); + found->second.actionType = static_cast(actionType); + if (gestureTracingEnabled()) { + LOG(INFO) << "[gesture-handler] attached handler=" << tag + << " view=" << found->second.viewTag + << " action=" << found->second.actionType; + } + } + } + + void setGestureHandlerConfig( + Runtime&, + double handlerTag, + folly::dynamic newConfig) { + const auto tag = static_cast(handlerTag); + auto config = configObject(std::move(newConfig)); + std::lock_guard lock(handlersMutex); + auto found = handlers.find(tag); + if (found != handlers.end()) + found->second.config = std::move(config); + } + + void updateGestureHandlerConfig( + Runtime&, + double handlerTag, + folly::dynamic newConfig) { + const auto tag = static_cast(handlerTag); + std::lock_guard lock(handlersMutex); + auto found = handlers.find(tag); + if (found != handlers.end() && newConfig.isObject()) { + for (const auto& [key, value] : newConfig.items()) + found->second.config[key] = value; + } + } + + void + configureRelations(Runtime&, double handlerTag, folly::dynamic relations) { + const auto tag = static_cast(handlerTag); + std::lock_guard lock(handlersMutex); + const auto found = handlers.find(tag); + if (found != handlers.end()) { + found->second.simultaneousHandlers = + relationTags(relations, "simultaneousHandlers"); + found->second.waitFor = relationTags(relations, "waitFor"); + found->second.blocksHandlers = relationTags(relations, "blocksHandlers"); + if (gestureTracingEnabled()) { + auto formatTags = [](const std::vector& tags) { + std::string result; + for (const auto value : tags) { + if (!result.empty()) + result += ","; + result += std::to_string(value); + } + return result; + }; + LOG(INFO) << "[gesture-handler] relations handler=" << tag + << " name=" << found->second.name << " simultaneous=" + << formatTags(found->second.simultaneousHandlers) + << " waitFor=" << formatTags(found->second.waitFor) + << " blocks=" << formatTags(found->second.blocksHandlers); + } + } + } + + void dropGestureHandler(Runtime&, double handlerTag) { + std::lock_guard lock(handlersMutex); + handlers.erase(static_cast(handlerTag)); + } + + void flushOperations(Runtime&) {} + + bool installUIRuntimeBindings(Runtime&) { + return true; + } +}; +} // namespace + +std::shared_ptr +create_gpui_gesture_handler_module( + const std::string& name, + std::shared_ptr jsInvoker) { + if (name != GPUIGestureHandlerModule::kModuleName) { + return nullptr; + } + rngpui::registerPointerObserver( + [](const rngpui::extension::RawPointerEvent& pointer) { + return processPointer(pointer); + }); + if (gestureTracingEnabled()) { + LOG(INFO) << "[gesture-handler] native pointer processor installed"; + } + return std::make_shared(std::move(jsInvoker)); +} diff --git a/vendor/react-native-gpui/modules/gesture-handler/package.json b/vendor/react-native-gpui/modules/gesture-handler/package.json new file mode 100644 index 0000000000..cf879d37ae --- /dev/null +++ b/vendor/react-native-gpui/modules/gesture-handler/package.json @@ -0,0 +1,22 @@ +{ + "name": "@react-native-gpui/gesture-handler", + "version": "0.1.0", + "description": "Native GPUI gesture recognition for react-native-gesture-handler", + "files": [ + "desktop", + "react-native.config.js", + "src" + ], + "peerDependencies": { + "react-native": "0.87.1", + "react-native-gesture-handler": "3.3.x", + "react-native-reanimated": "4.7.x", + "react-native-worklets": "0.13.x" + }, + "codegenConfig": { + "name": "GPUIGestureHandlerSpec", + "type": "modules", + "jsSrcsDir": "src" + }, + "license": "MIT" +} diff --git a/vendor/react-native-gpui/modules/gesture-handler/react-native.config.js b/vendor/react-native-gpui/modules/gesture-handler/react-native.config.js new file mode 100644 index 0000000000..d5e619bf42 --- /dev/null +++ b/vendor/react-native-gpui/modules/gesture-handler/react-native.config.js @@ -0,0 +1,25 @@ +module.exports = { + dependency: { + platforms: { + desktop: { + cmake: 'desktop', + cppProvider: 'create_gpui_gesture_handler_module', + componentCodegenPackage: 'react-native-gesture-handler', + componentDescriptors: [ + 'RNGestureHandlerButtonComponentDescriptor', + 'RNGestureHandlerRootViewComponentDescriptor', + ], + componentHeaders: [ + 'react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h', + ], + // Metro: replace these files of installed packages for gpui. + sourceOverrides: { + 'react-native-gesture-handler/src/handlers/customDirectEventTypes.ts': + './src/customDirectEventTypes.desktop.ts', + 'react-native-gesture-handler/src/v3/components/GestureButtons.tsx': + './src/GestureButtons.desktop.tsx', + }, + }, + }, + }, +}; diff --git a/vendor/react-native-gpui/modules/gesture-handler/src/GestureButtons.desktop.tsx b/vendor/react-native-gpui/modules/gesture-handler/src/GestureButtons.desktop.tsx new file mode 100644 index 0000000000..61b5db847e --- /dev/null +++ b/vendor/react-native-gpui/modules/gesture-handler/src/GestureButtons.desktop.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import './customDirectEventTypes.desktop'; +import { + Pressable, + type ColorValue, + type PressableProps, + type StyleProp, + type ViewStyle, +} from 'react-native'; + +type DesktopButtonProps = Omit & { + style?: StyleProp; + activeOpacity?: number; + underlayColor?: ColorValue; + onActiveStateChange?: (active: boolean) => void; + pressedStyle?: ViewStyle; + borderless?: boolean; + rippleColor?: ColorValue | number | null; + rippleRadius?: number | null; + foreground?: boolean; + exclusive?: boolean; + cancelable?: boolean; +}; + +function DesktopButton({ + activeOpacity = 0.75, + onActiveStateChange, + onPressIn, + onPressOut, + pressedStyle, + style, + ...props +}: DesktopButtonProps) { + return ( + [ + style, + pressed ? [{ opacity: activeOpacity }, pressedStyle] : undefined, + ]} + onPressIn={event => { + onActiveStateChange?.(true); + onPressIn?.(event); + }} + onPressOut={event => { + onActiveStateChange?.(false); + onPressOut?.(event); + }} + /> + ); +} + +export const RectButton = ({ underlayColor, ...props }: DesktopButtonProps) => ( + +); + +export const BorderlessButton = (props: DesktopButtonProps) => ( + +); + +export const RawButton = DesktopButton; +export const BaseButton = DesktopButton; +export const LegacyRectButton = RectButton; +export const LegacyBorderlessButton = BorderlessButton; +export const LegacyRawButton = RawButton; +export const LegacyBaseButton = BaseButton; diff --git a/vendor/react-native-gpui/modules/gesture-handler/src/NativeRNGestureHandlerModule.ts b/vendor/react-native-gpui/modules/gesture-handler/src/NativeRNGestureHandlerModule.ts new file mode 100644 index 0000000000..c9fe2cd0d0 --- /dev/null +++ b/vendor/react-native-gpui/modules/gesture-handler/src/NativeRNGestureHandlerModule.ts @@ -0,0 +1,32 @@ +import type { CodegenTypes, TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + createGestureHandler: ( + handlerName: string, + handlerTag: CodegenTypes.Double, + config: Object, + ) => void; + attachGestureHandler: ( + handlerTag: CodegenTypes.Double, + viewTag: CodegenTypes.Double, + actionType: CodegenTypes.Double, + ) => void; + setGestureHandlerConfig: ( + handlerTag: CodegenTypes.Double, + newConfig: Object, + ) => void; + updateGestureHandlerConfig: ( + handlerTag: CodegenTypes.Double, + newConfig: Object, + ) => void; + configureRelations: ( + handlerTag: CodegenTypes.Double, + relations: Object, + ) => void; + dropGestureHandler: (handlerTag: CodegenTypes.Double) => void; + flushOperations: () => void; + installUIRuntimeBindings: () => boolean; +} + +export default TurboModuleRegistry.getEnforcing('RNGestureHandlerModule'); diff --git a/vendor/react-native-gpui/modules/gesture-handler/src/customDirectEventTypes.desktop.ts b/vendor/react-native-gpui/modules/gesture-handler/src/customDirectEventTypes.desktop.ts new file mode 100644 index 0000000000..a27aed907e --- /dev/null +++ b/vendor/react-native-gpui/modules/gesture-handler/src/customDirectEventTypes.desktop.ts @@ -0,0 +1,13 @@ +// RNGH's legacy handler path registers only the continuous event in its JS +// module. GPUI dispatches the codegen state-change event directly, so expose +// both direct-event registrations from this platform package. +import { customDirectEventTypes } from 'react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry'; + +customDirectEventTypes.topGestureHandlerEvent = { + registrationName: 'onGestureHandlerEvent', +}; +customDirectEventTypes.topGestureHandlerStateChange = { + registrationName: 'onGestureHandlerStateChange', +}; + +export { customDirectEventTypes }; diff --git a/vendor/react-native-gpui/modules/reanimated/desktop/CMakeLists.txt b/vendor/react-native-gpui/modules/reanimated/desktop/CMakeLists.txt new file mode 100644 index 0000000000..cb9d7257db --- /dev/null +++ b/vendor/react-native-gpui/modules/reanimated/desktop/CMakeLists.txt @@ -0,0 +1,94 @@ +if(NOT RN_GPUI_PROJECT_ROOT) + message(FATAL_ERROR + "@react-native-gpui/reanimated was added by the platform; RN_GPUI_PROJECT_ROOT must be set by its build.rs") +endif() +set(_gpui_app_root "${RN_GPUI_PROJECT_ROOT}") +find_program(_gpui_node node REQUIRED) +execute_process( + COMMAND "${_gpui_node}" -e "process.stdout.write(require.resolve('react-native-reanimated/package.json', {paths: [process.argv[1]]}))" "${_gpui_app_root}" + RESULT_VARIABLE _reanimated_resolve_result + OUTPUT_VARIABLE _reanimated_package_json + OUTPUT_STRIP_TRAILING_WHITESPACE) +if(NOT _reanimated_resolve_result EQUAL 0 OR NOT EXISTS "${_reanimated_package_json}") + message(FATAL_ERROR "react-native-reanimated could not be resolved from ${_gpui_app_root}") +endif() +get_filename_component(_reanimated_root "${_reanimated_package_json}" DIRECTORY) +execute_process( + COMMAND "${_gpui_node}" -e "process.stdout.write(require.resolve('react-native/package.json', {paths: [process.argv[1]]}))" "${_gpui_app_root}" + RESULT_VARIABLE _react_native_resolve_result + OUTPUT_VARIABLE _react_native_package_json + OUTPUT_STRIP_TRAILING_WHITESPACE) +if(NOT _react_native_resolve_result EQUAL 0 OR NOT EXISTS "${_react_native_package_json}") + message(FATAL_ERROR "react-native could not be resolved from ${_gpui_app_root}") +endif() +get_filename_component(_react_native_root "${_react_native_package_json}" DIRECTORY) +execute_process( + COMMAND "${_gpui_node}" -e "process.stdout.write(require.resolve('react-native-worklets/package.json', {paths: [process.argv[1]]}))" "${_gpui_app_root}" + RESULT_VARIABLE _worklets_resolve_result + OUTPUT_VARIABLE _worklets_package_json + OUTPUT_STRIP_TRAILING_WHITESPACE) +if(NOT _worklets_resolve_result EQUAL 0 OR NOT EXISTS "${_worklets_package_json}") + message(FATAL_ERROR "react-native-worklets could not be resolved from ${_gpui_app_root}") +endif() +get_filename_component(_worklets_root "${_worklets_package_json}" DIRECTORY) +set(_reanimated_common "${_reanimated_root}/Common/cpp") +set(_worklets_common "${_worklets_root}/Common/cpp") +if(NOT EXISTS "${_reanimated_common}/reanimated/NativeModules/ReanimatedModuleProxy.h") + message(FATAL_ERROR "react-native-reanimated 4.7.x was not found in ${_gpui_app_root} or any ancestor node_modules directory") +endif() +execute_process( + COMMAND "${_gpui_node}" -e "process.stdout.write(require.resolve('@react-native-gpui/worklets/package.json', {paths: [process.argv[1]]}))" "${_gpui_app_root}" + RESULT_VARIABLE _gpui_worklets_resolve_result + OUTPUT_VARIABLE _gpui_worklets_package_json + OUTPUT_STRIP_TRAILING_WHITESPACE) +if(NOT _gpui_worklets_resolve_result EQUAL 0 OR NOT EXISTS "${_gpui_worklets_package_json}") + message(FATAL_ERROR "@react-native-gpui/worklets could not be resolved from ${_gpui_app_root}") +endif() +get_filename_component(_gpui_worklets_root "${_gpui_worklets_package_json}" DIRECTORY) +if(NOT EXISTS "${_worklets_common}/worklets/Compat/StableApi.h") + message(FATAL_ERROR "react-native-worklets 0.13.x is required by GPUI Reanimated") +endif() + +# Match the static feature-flag definition normally supplied by Reanimated's +# CocoaPods/Gradle builds. Without this, FeatureFlags.h silently treats every +# flag as false, including defaults which change how animated props are +# committed through Fabric. +execute_process( + COMMAND "${_gpui_node}" -e + "const fs=require('fs'); const flags=JSON.parse(fs.readFileSync(process.argv[1],'utf8')); const app=JSON.parse(fs.readFileSync(process.argv[2],'utf8')); Object.assign(flags, app.reanimated?.staticFeatureFlags ?? {}); if(process.platform==='darwin' && !Object.hasOwn(app.reanimated?.staticFeatureFlags ?? {}, 'IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS')) flags.IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS=true; if(process.platform==='linux' && !Object.hasOwn(app.reanimated?.staticFeatureFlags ?? {}, 'LINUX_SYNCHRONOUSLY_UPDATE_UI_PROPS')) flags.LINUX_SYNCHRONOUSLY_UPDATE_UI_PROPS=true; process.stdout.write(Object.entries(flags).map(([key,value])=>'['+key+':'+value+']').join(''));" + "${_reanimated_root}/src/featureFlags/staticFlags.json" + "${_gpui_app_root}/package.json" + RESULT_VARIABLE _reanimated_flags_result + OUTPUT_VARIABLE _reanimated_feature_flags + OUTPUT_STRIP_TRAILING_WHITESPACE +) +if(NOT _reanimated_flags_result EQUAL 0 OR _reanimated_feature_flags STREQUAL "") + message(FATAL_ERROR "Failed to load Reanimated static feature flags for GPUI") +endif() + +execute_process( + COMMAND "${_gpui_node}" "${_react_native_root}/scripts/generate-codegen-artifacts.js" + --path "${_gpui_app_root}" + --targetPlatform ios + --outputPath "${_gpui_app_root}/desktop/build/codegen" + --source app + RESULT_VARIABLE _reanimated_codegen_result +) +if(NOT _reanimated_codegen_result EQUAL 0) + message(FATAL_ERROR "React Native codegen failed while configuring GPUI Reanimated") +endif() +set(_rn_codegen "${_gpui_app_root}/desktop/build/codegen/build/generated/ios/ReactCodegen") + +file(GLOB_RECURSE _reanimated_sources CONFIGURE_DEPENDS "${_reanimated_common}/reanimated/*.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES + ${_reanimated_sources} + "${CMAKE_CURRENT_LIST_DIR}/cpp/GPUIReanimatedModule.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS + "${_reanimated_common}" + "${_reanimated_root}" + "${_worklets_common}" + "${_rn_codegen}" + "${_gpui_worklets_root}/desktop/cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_DEFINITIONS + "REANIMATED_VERSION=4.7.1" + "REANIMATED_FEATURE_FLAGS=\"${_reanimated_feature_flags}\"") diff --git a/vendor/react-native-gpui/modules/reanimated/desktop/cpp/GPUIReanimatedModule.cpp b/vendor/react-native-gpui/modules/reanimated/desktop/cpp/GPUIReanimatedModule.cpp new file mode 100644 index 0000000000..2e8ded524b --- /dev/null +++ b/vendor/react-native-gpui/modules/reanimated/desktop/cpp/GPUIReanimatedModule.cpp @@ -0,0 +1,161 @@ +#include "GPUIWorkletsModule.h" +#include "GpuiHost.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace { +bool animationTracingEnabled() { + static const bool enabled = std::getenv("RN_GPUI_DEBUG_ANIMATION") != nullptr; + return enabled; +} + +using facebook::jsi::Runtime; +using reanimated::PlatformDepMethodsHolder; +using reanimated::ReanimatedModuleProxy; + +PlatformDepMethodsHolder platformMethods( + const std::shared_ptr>& proxyHolder) { + PlatformDepMethodsHolder methods; + methods.requestRender = [](std::function callback) { + static std::atomic requests{0}; + const auto count = ++requests; + if (animationTracingEnabled() && (count <= 5 || count % 60 == 0)) { + LOG(INFO) << "[gpui-reanimated] requestRender count=" << count; + } + auto module = gpui_worklets::currentModule(); + if (module) + module->requestAnimationFrame(std::move(callback)); + }; + methods.synchronouslyUpdateUIPropsFunction = [](const int tag, + const folly::dynamic& props) { + rngpui::gpui_synchronously_update_props(tag, props); + }; + methods.getAnimationTimestamp = [] { + return std::chrono::duration( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + }; + methods.registerSensor = + [](int, int, int, std::function) { return 0; }; + methods.unregisterSensor = [](int) {}; + methods.setGestureStateFunction = [](int, int) {}; + methods.maybeFlushUIUpdatesQueueFunction = [proxyHolder] { + if (auto proxy = proxyHolder->lock()) { + proxy->performOperations(); + } + }; + methods.attachPseudoSelector = [](facebook::react::Tag, + reanimated::PseudoSelector, + std::function callback) { + if (callback) + callback(false); + }; + methods.detachPseudoSelector = [](facebook::react::Tag, + reanimated::PseudoSelector) {}; +#if defined(__APPLE__) + methods.forceScreenSnapshotFunction = [](facebook::react::Tag) { + return false; + }; + methods.readMountedViewPropsFunction = + [](facebook::react::Tag) -> facebook::react::Props::Shared { + return nullptr; + }; +#endif + return methods; +} + +class Module final + : public facebook::react::NativeReanimatedModuleSpec { + public: + explicit Module(std::shared_ptr jsInvoker) + : NativeReanimatedModuleSpec(std::move(jsInvoker)) {} + + bool installTurboModule(Runtime& runtime) { + if (proxy_) + return true; + auto worklets = gpui_worklets::currentModule(); + auto uiManager = rngpui::get_gpui_ui_manager(); + if (!worklets || !worklets->uiRuntime() || !worklets->uiScheduler() || + !uiManager) { + throw facebook::jsi::JSError( + runtime, "GPUI Reanimated requires initialized Worklets and Fabric"); + } + + // The platform flush callback is constructed before the proxy exists. + // Retain only a weak reference so the decorated UI runtime cannot keep + // the module alive after React Native tears it down. + auto proxyHolder = std::make_shared>(); + auto methods = platformMethods(proxyHolder); + proxy_ = std::make_shared( + worklets->uiRuntime(), + worklets->uiScheduler(), + runtime, + jsInvoker_, + methods, + false); + *proxyHolder = proxy_; + proxy_->init(methods); + proxy_->initializeFabric(uiManager); + LOG(INFO) << "[gpui-reanimated] module proxy initialized"; + auto& uiRuntime = + worklets::getJSIRuntimeFromWorkletRuntime(worklets->uiRuntime()); + reanimated::RNRuntimeDecorator::decorate(runtime, uiRuntime, proxy_); + std::weak_ptr weakProxy = proxy_; + eventListener_ = std::make_shared( + [weakProxy](const facebook::react::RawEvent& event) { + auto proxy = weakProxy.lock(); + if (!proxy) + return false; + const auto timestamp = + std::chrono::duration( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + static std::atomic events{0}; + const auto count = ++events; + if (animationTracingEnabled() && + event.type.find("GestureHandler") != std::string::npos) { + const auto* target = event.eventTarget.get(); + const auto tag = target ? target->getTag() : -1; + auto eventName = event.type; + if (eventName.rfind("top", 0) == 0) { + eventName = "on" + eventName.substr(3); + } + LOG(INFO) << "[gpui-reanimated] gesture lookup type=" << eventName + << " tag=" << tag << " waiting=" + << proxy->isAnyHandlerWaitingForEvent(eventName, tag); + } + if (animationTracingEnabled() && + (count <= 10 || + event.type.find("GestureHandler") != std::string::npos)) { + LOG(INFO) << "[gpui-reanimated] raw event count=" << count + << " type=" << event.type; + } + return proxy->handleRawEvent(event, timestamp); + }); + rngpui::register_gpui_event_listener(eventListener_); + return true; + } + + std::shared_ptr proxy_; + std::shared_ptr eventListener_; +}; +} // namespace + +std::shared_ptr create_gpui_reanimated_module( + const std::string& name, + std::shared_ptr jsInvoker) { + return rngpui::provideTurboModule(name, std::move(jsInvoker)); +} diff --git a/vendor/react-native-gpui/modules/reanimated/package.json b/vendor/react-native-gpui/modules/reanimated/package.json new file mode 100644 index 0000000000..b5dd81d6b3 --- /dev/null +++ b/vendor/react-native-gpui/modules/reanimated/package.json @@ -0,0 +1,22 @@ +{ + "name": "@react-native-gpui/reanimated", + "version": "0.1.0", + "description": "GPUI platform bindings for Reanimated 4", + "files": [ + "desktop", + "react-native.config.js", + "src" + ], + "peerDependencies": { + "react-native": "0.87.1", + "react-native-reanimated": "4.7.x", + "react-native-worklets": "0.13.x", + "@react-native-gpui/worklets": "0.1.x" + }, + "codegenConfig": { + "name": "GPUIReanimatedSpec", + "type": "modules", + "jsSrcsDir": "src" + }, + "license": "MIT" +} diff --git a/vendor/react-native-gpui/modules/reanimated/react-native.config.js b/vendor/react-native-gpui/modules/reanimated/react-native.config.js new file mode 100644 index 0000000000..edd87f97d4 --- /dev/null +++ b/vendor/react-native-gpui/modules/reanimated/react-native.config.js @@ -0,0 +1,10 @@ +module.exports = { + dependency: { + platforms: { + desktop: { + cmake: 'desktop', + cppProvider: 'create_gpui_reanimated_module', + }, + }, + }, +}; diff --git a/vendor/react-native-gpui/modules/reanimated/src/NativeReanimatedModule.ts b/vendor/react-native-gpui/modules/reanimated/src/NativeReanimatedModule.ts new file mode 100644 index 0000000000..a664c6dca4 --- /dev/null +++ b/vendor/react-native-gpui/modules/reanimated/src/NativeReanimatedModule.ts @@ -0,0 +1,8 @@ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + installTurboModule: () => boolean; +} + +export default TurboModuleRegistry.get('ReanimatedModule'); diff --git a/vendor/react-native-gpui/modules/safe-area-context/desktop/CMakeLists.txt b/vendor/react-native-gpui/modules/safe-area-context/desktop/CMakeLists.txt new file mode 100644 index 0000000000..5e934c3598 --- /dev/null +++ b/vendor/react-native-gpui/modules/safe-area-context/desktop/CMakeLists.txt @@ -0,0 +1,33 @@ +if(NOT RN_GPUI_PROJECT_ROOT) + message(FATAL_ERROR + "@react-native-gpui/safe-area-context was added by the platform; RN_GPUI_PROJECT_ROOT must be set by its build.rs") +endif() +rn_gpui_find_node_module(react-native-safe-area-context _safe_area_root) +set(_safe_area_common "${_safe_area_root}/common/cpp") +set(_safe_area_shadow_node "${_safe_area_common}/react/renderer/components/safeareacontext/RNCSafeAreaViewShadowNode.cpp") +set(_safe_area_package_json "${_safe_area_root}/package.json") + +if(NOT EXISTS "${_safe_area_shadow_node}" OR NOT EXISTS "${_safe_area_package_json}") + message(FATAL_ERROR "react-native-safe-area-context 5.10.x was not found at ${_safe_area_root}") +endif() +file(READ "${_safe_area_package_json}" _safe_area_package_json_contents) +string(JSON _safe_area_version GET "${_safe_area_package_json_contents}" version) +if(NOT _safe_area_version MATCHES "^5\\.10\\.") + message(FATAL_ERROR "@react-native-gpui/safe-area-context supports react-native-safe-area-context 5.10.x, found ${_safe_area_version}") +endif() + +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES + "${_safe_area_shadow_node}" + "${CMAKE_CURRENT_LIST_DIR}/cpp/GPUISafeAreaContextModule.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS + "${_safe_area_common}" + "${RN_GPUI_REACT_CXX_PLATFORM_DIR}" + "${CMAKE_CURRENT_LIST_DIR}/cpp") +# Safe-area insets reach the mounting manager through this glue, which the +# platform compiles only because the package is installed. +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/cpp/GpuiSafeAreaModule.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INSTALLERS safe_area) +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INCLUDE_DIRS + "${CMAKE_CURRENT_LIST_DIR}/cpp" + "${RN_GPUI_PROJECT_ROOT}/desktop/autolink/codegen/safeareacontext") diff --git a/vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GPUISafeAreaContextModule.cpp b/vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GPUISafeAreaContextModule.cpp new file mode 100644 index 0000000000..670865ad27 --- /dev/null +++ b/vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GPUISafeAreaContextModule.cpp @@ -0,0 +1,48 @@ +#include +#include +#include "GPUISafeAreaInsets.h" + +#include + +namespace { +using facebook::react::TurboModule; + +class SafeAreaContextModule final + : public facebook::react::NativeSafeAreaContextSpec { + public: + explicit SafeAreaContextModule( + std::shared_ptr jsInvoker) + : NativeSafeAreaContextSpec(std::move(jsInvoker)) {} + + facebook::jsi::Object getConstants(facebook::jsi::Runtime& runtime) { + const auto windowSize = facebook::react::DeviceInfoModule::windowSize(); + facebook::jsi::Object insets(runtime); + const auto safeInsets = gpui_safe_area::gpui_safe_area_insets(0); + insets.setProperty(runtime, "top", safeInsets.top); + insets.setProperty(runtime, "right", safeInsets.right); + insets.setProperty(runtime, "bottom", safeInsets.bottom); + insets.setProperty(runtime, "left", safeInsets.left); + + facebook::jsi::Object frame(runtime); + frame.setProperty(runtime, "x", 0.0); + frame.setProperty(runtime, "y", 0.0); + frame.setProperty(runtime, "width", windowSize.width); + frame.setProperty(runtime, "height", windowSize.height); + + facebook::jsi::Object metrics(runtime); + metrics.setProperty(runtime, "insets", std::move(insets)); + metrics.setProperty(runtime, "frame", std::move(frame)); + + facebook::jsi::Object constants(runtime); + constants.setProperty(runtime, "initialWindowMetrics", std::move(metrics)); + return constants; + } +}; +} // namespace + +std::shared_ptr create_gpui_safe_area_context_module( + const std::string& name, + std::shared_ptr jsInvoker) { + return rngpui::provideTurboModule( + name, std::move(jsInvoker)); +} diff --git a/vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GPUISafeAreaInsets.h b/vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GPUISafeAreaInsets.h new file mode 100644 index 0000000000..cd432b1e7c --- /dev/null +++ b/vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GPUISafeAreaInsets.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace gpui_safe_area { + +struct Insets { + double top; + double right; + double bottom; + double left; +}; + +inline std::atomic windowTopInset{0.0}; + +inline Insets gpui_safe_area_insets(int32_t /*surfaceId*/) { + return { + .top = windowTopInset.load(), .right = 0.0, .bottom = 0.0, .left = 0.0}; +} + +} // namespace gpui_safe_area diff --git a/vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GpuiSafeAreaModule.cpp b/vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GpuiSafeAreaModule.cpp new file mode 100644 index 0000000000..4c84f883c6 --- /dev/null +++ b/vendor/react-native-gpui/modules/safe-area-context/desktop/cpp/GpuiSafeAreaModule.cpp @@ -0,0 +1,143 @@ +#include "GPUISafeAreaInsets.h" +#include "GpuiModules.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace rngpui { +using namespace facebook::react; + +namespace { +struct TrackedView { + std::array frame; + SharedEventEmitter emitter; + std::shared_ptr> state; +}; + +std::mutex trackedMutex; +std::unordered_map tracked; + +std::shared_ptr> safeAreaState( + const ShadowView& view) { + if (std::string_view(view.componentName) != "RNCSafeAreaView") + return nullptr; + return std::dynamic_pointer_cast>( + view.state); +} + +void dispatchInsets( + const SharedEventEmitter& emitter, + const std::array& frame, + float top, + float right, + float bottom, + float left) { + if (!emitter) + return; + emitter->dispatchEvent( + "insetsChange", + folly::dynamic::object( + "insets", + folly::dynamic::object("top", top)("right", right)("bottom", bottom)( + "left", left))( + "frame", + folly::dynamic::object("x", frame[0])("y", frame[1])( + "width", frame[2])("height", frame[3]))); +} + +void applyInsets(float top, float right, float bottom, float left) { + const bool changed = gpui_safe_area::windowTopInset.exchange(top) != top; + std::vector>> + states; + { + std::lock_guard lock(trackedMutex); + for (const auto& [tag, entry] : tracked) { + (void)tag; + if (changed && entry.emitter) + dispatchInsets(entry.emitter, entry.frame, top, right, bottom, left); + if (!entry.state) + continue; + // The tracked state is the one the view had when its frame last changed. + // A state update does not move the frame, so it would stay at the old + // insets, and every batch would send the same update again: one commit + // per batch, for ever. + const auto latest = + std::dynamic_pointer_cast>( + entry.state->getMostRecentState()); + const auto& current = latest ? latest : entry.state; + if (current->getData().insets.top != top) + states.push_back(current); + } + } + for (const auto& state : states) { + auto next = state->getData(); + next.insets.top = top; + state->updateState(std::move(next)); + } +} +} // namespace + +void rn_gpui_install_safe_area_module() { + registerMountObserver(extension::MountObserver{ + .onFrameChanged = + [](SurfaceId surfaceId, const ShadowView& view) { + const auto& frame = view.layoutMetrics.frame; + const std::array nextFrame = { + frame.origin.x, + frame.origin.y, + frame.size.width, + frame.size.height}; + const bool isProvider = + std::string_view(view.componentName) == "RNCSafeAreaProvider"; + if (!isProvider && !safeAreaState(view)) + return; + { + std::lock_guard lock(trackedMutex); + auto& entry = tracked[view.tag]; + if (isProvider && entry.emitter && entry.frame == nextFrame) + return; + entry.frame = nextFrame; + entry.emitter = view.eventEmitter; + entry.state = safeAreaState(view); + } + if (!isProvider) + return; + const auto insets = gpui_safe_area::gpui_safe_area_insets( + static_cast(surfaceId)); + LOG(INFO) << "[gpui-safe-area] onInsetsChange tag=" << view.tag + << " surface=" << surfaceId << " frame=(" + << frame.origin.x << ',' << frame.origin.y << ' ' + << frame.size.width << 'x' << frame.size.height << ')'; + dispatchInsets( + view.eventEmitter, + nextFrame, + insets.top, + insets.right, + insets.bottom, + insets.left); + }, + .onDeleted = + [](Tag tag) { + std::lock_guard lock(trackedMutex); + tracked.erase(tag); + }, + .afterBatch = + [] { + applyInsets( + gpui_safe_area::windowTopInset.load(), 0.0f, 0.0f, 0.0f); + }}); + + registerWindowInsetsObserver( + [](float top, float right, float bottom, float left) { + applyInsets(top, right, bottom, left); + }); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/modules/safe-area-context/package.json b/vendor/react-native-gpui/modules/safe-area-context/package.json new file mode 100644 index 0000000000..1f1aacab20 --- /dev/null +++ b/vendor/react-native-gpui/modules/safe-area-context/package.json @@ -0,0 +1,20 @@ +{ + "name": "@react-native-gpui/safe-area-context", + "version": "0.1.0", + "description": "GPUI platform implementation for react-native-safe-area-context", + "files": [ + "desktop", + "react-native.config.js", + "src" + ], + "peerDependencies": { + "react-native": "0.87.1", + "react-native-safe-area-context": "5.10.x" + }, + "codegenConfig": { + "name": "GPUISafeAreaContextSpec", + "type": "modules", + "jsSrcsDir": "src" + }, + "license": "MIT" +} diff --git a/vendor/react-native-gpui/modules/safe-area-context/react-native.config.js b/vendor/react-native-gpui/modules/safe-area-context/react-native.config.js new file mode 100644 index 0000000000..2ab45e0071 --- /dev/null +++ b/vendor/react-native-gpui/modules/safe-area-context/react-native.config.js @@ -0,0 +1,19 @@ +module.exports = { + dependency: { + platforms: { + desktop: { + cmake: 'desktop', + cppProvider: 'create_gpui_safe_area_context_module', + componentCodegenPackage: 'react-native-safe-area-context', + componentDescriptors: [ + 'RNCSafeAreaProviderComponentDescriptor', + 'RNCSafeAreaViewComponentDescriptor', + ], + componentHeaders: [ + 'react/renderer/components/safeareacontext/ComponentDescriptors.h', + 'react/renderer/components/safeareacontext/RNCSafeAreaViewComponentDescriptor.h', + ], + }, + }, + }, +}; diff --git a/vendor/react-native-gpui/modules/safe-area-context/src/NativeSafeAreaContext.ts b/vendor/react-native-gpui/modules/safe-area-context/src/NativeSafeAreaContext.ts new file mode 100644 index 0000000000..282ab56909 --- /dev/null +++ b/vendor/react-native-gpui/modules/safe-area-context/src/NativeSafeAreaContext.ts @@ -0,0 +1,23 @@ +import type { CodegenTypes, TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + getConstants: () => { + initialWindowMetrics?: { + insets: { + top: CodegenTypes.Double; + right: CodegenTypes.Double; + bottom: CodegenTypes.Double; + left: CodegenTypes.Double; + }; + frame: { + x: CodegenTypes.Double; + y: CodegenTypes.Double; + width: CodegenTypes.Double; + height: CodegenTypes.Double; + }; + }; + }; +} + +export default TurboModuleRegistry.get('RNCSafeAreaContext'); diff --git a/vendor/react-native-gpui/modules/screens/desktop/CMakeLists.txt b/vendor/react-native-gpui/modules/screens/desktop/CMakeLists.txt new file mode 100644 index 0000000000..9b0d721758 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/CMakeLists.txt @@ -0,0 +1,30 @@ +if(NOT RN_GPUI_PROJECT_ROOT) + message(FATAL_ERROR + "@react-native-gpui/screens was added by the platform; RN_GPUI_PROJECT_ROOT must be set by its build.rs") +endif() +rn_gpui_find_node_module(react-native-screens _screens_root) +set(_screens_common "${_screens_root}/common/cpp") +set(_screens_component_dir "${_screens_common}/react/renderer/components/rnscreens") +set(_screens_package_json "${_screens_root}/package.json") + +if(NOT EXISTS "${_screens_component_dir}/RNSScreenShadowNode.cpp" OR NOT EXISTS "${_screens_package_json}") + message(FATAL_ERROR "react-native-screens 4.28.x was not found at ${_screens_root}") +endif() + +file(READ "${_screens_package_json}" _screens_package_json_contents) +string(JSON _screens_version GET "${_screens_package_json_contents}" version) +if(NOT _screens_version MATCHES "^4\\.28\\.") + message(FATAL_ERROR "@react-native-gpui/screens supports react-native-screens 4.28.x, found ${_screens_version}") +endif() + +file(GLOB _screens_common_sources CONFIGURE_DEPENDS "${_screens_component_dir}/*.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES ${_screens_common_sources}) +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS "${_screens_common}") +# The mount pipeline knows nothing about screens: this glue fills the header and +# screen fields of a mount op, and the platform compiles it only because the +# package is installed. +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/cpp/GpuiScreensModule.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INSTALLERS screens) +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INCLUDE_DIRS + "${_screens_common}") diff --git a/vendor/react-native-gpui/modules/screens/desktop/Cargo.toml b/vendor/react-native-gpui/modules/screens/desktop/Cargo.toml new file mode 100644 index 0000000000..fc62ad9ff2 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "rn-gpui-screens" +version = "0.1.0" +edition = "2021" + +[dependencies] +react-native-gpui = "0.1" +rn-gpui-sys = "0.1" +gpui = "0.2" +serde_json = "1" +log = "0.4" + +[dev-dependencies] +# `TestNode` and `TreeView::for_test` are gated behind `test-driver`, and a +# dependency cannot enable another package's feature. It is a dev-dependency +# rather than a normal one so a build of the module never pays for it, and it only +# becomes usable because this crate is its own workspace root below. +react-native-gpui = { version = "0.1", features = ["test-driver"] } + +# Its own workspace root, deliberately. Cargo refuses to test a package that has +# dev-dependencies unless it is a workspace member, and core's workspace cannot +# hold it: a module resolves core by version and must not be a member of the +# checkout's workspace. A root with itself as the only member satisfies the rule +# without making the module part of anything else. +[workspace] + +# The checkout's shared dev profile (see core's `ReactGpui/Cargo.toml`): line tables +# only, no debug info for dependencies, which are optimized. Every workspace root +# builds into the one root `target/`, so a matching profile is also what lets them +# reuse each other's dependency builds instead of storing one copy each. +[profile.dev] +debug = "line-tables-only" +split-debuginfo = "packed" + +[profile.dev.package."*"] +debug = false +opt-level = 3 + +[profile.dev.package.rn-gpui-sys] +opt-level = 0 + +# This block has to live here because Cargo honours `[patch.crates-io]` only at a +# workspace root, and this crate is one. `npm run test:rust` regenerates it from +# core's manifest before running these tests and fails if it changed, so the gpui +# revision is never written down by hand — commit whatever the run rewrites. +[patch.crates-io] +# Managed by @react-native-gpui/cli autolink; do not edit between these markers. +react-native-gpui = { path = "../../../packages/core/ReactGpui/rn-gpui" } +rn-gpui-sys = { path = "../../../packages/core/ReactGpui/rn-gpui-sys" } +rn-gpui-module = { path = "../../../packages/core/ReactGpui/rn-gpui-module" } +rn-gpui-module-build = { path = "../../../packages/core/ReactGpui/rn-gpui-module-build" } +gpui = { git = "https://github.com/streetwriters/gpui-fast", rev = "8da7d89" } +gpui_platform = { git = "https://github.com/streetwriters/gpui-fast", rev = "8da7d89" } +# End managed by autolink. diff --git a/vendor/react-native-gpui/modules/screens/desktop/build.rs b/vendor/react-native-gpui/modules/screens/desktop/build.rs new file mode 100644 index 0000000000..b40f68662d --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/build.rs @@ -0,0 +1,38 @@ +//! Puts the C++ runtime's shared libraries on this crate's test binary rpath. +//! +//! `rn-gpui-screens`'s tests build gpui trees, which link `libjsi.dylib` and the +//! rest of the Node-API runtime. Core's `build.rs` emits +//! `cargo:rustc-link-arg=-Wl,-rpath,` for its own binary — the app — and that +//! flag is not inherited by a dependency's test binary, so the test process +//! aborted in dyld with `Library not loaded: @rpath/libjsi.dylib` before running a +//! single test. +//! +//! Core exports the directories it was given as `DEP_RN_GPUI_CORE_RPATHS`, which +//! is the same variable core's own `build.rs` reads. Re-emitting it here scopes +//! the flag to this crate's test binary, since `cargo:rustc-link-arg` without a +//! target suffix applies to the crate's own targets and not to the app that +//! depends on it. + +fn main() { + println!("cargo:rerun-if-env-changed=DEP_RN_GPUI_CORE_RPATHS"); + println!("cargo:rerun-if-changed=build.rs"); + + let Ok(dirs) = std::env::var("DEP_RN_GPUI_CORE_RPATHS") else { + return; + }; + let dirs = dirs + .split('|') + .filter(|dir| !dir.is_empty()) + .collect::>(); + if dirs.is_empty() { + return; + } + // MSVC and MinGW have no rpath, and the flag is rejected outright; Windows + // finds the DLLs through PATH, which the test runner already arranges. + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + return; + } + for dir in dirs { + println!("cargo:rustc-link-arg=-Wl,-rpath,{dir}"); + } +} diff --git a/vendor/react-native-gpui/modules/screens/desktop/cpp/GpuiScreensModule.cpp b/vendor/react-native-gpui/modules/screens/desktop/cpp/GpuiScreensModule.cpp new file mode 100644 index 0000000000..31f198b876 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/cpp/GpuiScreensModule.cpp @@ -0,0 +1,77 @@ + +#include "GpuiModules.h" + +#include +#include + +#include + +namespace rngpui { +namespace { + +using namespace facebook::react; + +std::string screenPropsJson( + const std::string& /*componentName*/, + const ShadowView& view) { + if (!view.props) { + return std::string("{}"); + } + auto props = std::dynamic_pointer_cast(view.props); + if (!props) { + return std::string("{}"); + } + folly::dynamic object = folly::dynamic::object; + object["activityState"] = props->activityState; + object["stackAnimation"] = toString(props->stackAnimation); + object["stackPresentation"] = toString(props->stackPresentation); + return folly::toJson(object); +} + +std::string headerConfigPropsJson( + const std::string& /*componentName*/, + const ShadowView& view) { + if (!view.props) { + return std::string("{}"); + } + auto props = std::dynamic_pointer_cast( + view.props); + if (!props) { + return std::string("{}"); + } + folly::dynamic object = folly::dynamic::object; + object["title"] = props->title; + object["backgroundColor"] = argb(props->backgroundColor); + object["titleColor"] = argb(props->titleColor); + object["tintColor"] = argb(props->color); + object["hidden"] = props->hidden; + object["hideBackButton"] = props->hideBackButton; + return folly::toJson(object); +} + +std::string headerSubviewPropsJson( + const std::string& /*componentName*/, + const ShadowView& view) { + if (!view.props) { + return std::string("{}"); + } + auto props = + std::dynamic_pointer_cast( + view.props); + if (!props) { + return std::string("{}"); + } + folly::dynamic object = folly::dynamic::object; + object["type"] = toString(props->type); + return folly::toJson(object); +} + +} // namespace + +void rn_gpui_install_screens_module() { + registerComponentProps("RNSScreen", screenPropsJson); + registerComponentProps("RNSScreenStackHeaderConfig", headerConfigPropsJson); + registerComponentProps("RNSScreenStackHeaderSubview", headerSubviewPropsJson); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/modules/screens/desktop/src/lib.rs b/vendor/react-native-gpui/modules/screens/desktop/src/lib.rs new file mode 100644 index 0000000000..26209f4b81 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/src/lib.rs @@ -0,0 +1,29 @@ +//! The screens module. +//! +//! Everything react-native-screens needs lives here: its props extraction in +//! `cpp/`, and its layout, painting and lifecycle in the modules below. Core is +//! told only what any library can say — a component name, a props reader, and a +//! set of generic hooks. +pub mod lifecycle; +pub mod props; +pub mod render; +pub mod stack; +pub mod state; +pub mod transition; +pub mod unsupported; + +/// Registers this module's renderers and observer. +/// +/// Autolink calls this from `ensure_linked()` while `main` starts, before any +/// `gpui::App` exists, so nothing here may touch the app. +pub fn rn_gpui_register() { + react_native_gpui::extensions::register_store_observer(std::sync::Arc::new( + stack::StackObserver, + )); + for (component, build) in render::RENDERED_COMPONENTS { + react_native_gpui::extensions::register_component_renderer( + *component, + std::sync::Arc::from(build()), + ); + } +} diff --git a/vendor/react-native-gpui/modules/screens/desktop/src/lifecycle.rs b/vendor/react-native-gpui/modules/screens/desktop/src/lifecycle.rs new file mode 100644 index 0000000000..887ff23534 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/src/lifecycle.rs @@ -0,0 +1,80 @@ +//! The lifecycle events a stack sends to its screens as they appear and leave. +//! +//! React Native exposes these as `topWillAppear`, `topAppear`, +//! `topWillDisappear` and `topDisappear`, in that order around a transition. +use gpui::App; +use react_native_gpui::extensions::StoreHandle; + +/// The events an activity-state change produces, in the order they fire. +pub fn lifecycle_events(previous: f32, next: f32) -> &'static [&'static str] { + match (previous < 1.0, previous >= 2.0, next < 1.0, next >= 2.0) { + (true, _, false, true) => &["topWillAppear", "topAppear"], + (true, _, false, false) => &["topWillAppear"], + (_, true, false, false) => &["topWillDisappear"], + (false, false, true, _) => &["topDisappear"], + (false, true, true, _) => &["topWillDisappear", "topDisappear"], + (false, false, false, true) => &["topAppear"], + _ => &[], + } +} + +/// Sends the "will" half of a transition: the outgoing screen then the incoming. +pub fn dispatch_will( + handle: &StoreHandle, + outgoing: Option, + incoming: Option, + cx: &mut App, +) { + for (tag, event) in [ + outgoing.map(|tag| (tag, "topWillDisappear")), + incoming.map(|tag| (tag, "topWillAppear")), + ] + .into_iter() + .flatten() + { + log::info!("[screens] tag={tag} lifecycle={event}"); + handle.dispatch_event(tag, event, &serde_json::json!({}), cx); + } +} + +/// Sends the "did" half: the outgoing screen then the incoming, and wakes +/// Fabric so the callbacks arrive before a later pop unmounts them. +pub fn dispatch_did( + handle: &StoreHandle, + outgoing: Option, + incoming: Option, + cx: &mut App, +) { + for (tag, event) in [ + outgoing.map(|tag| (tag, "topDisappear")), + incoming.map(|tag| (tag, "topAppear")), + ] + .into_iter() + .flatten() + { + log::info!("[screens] tag={tag} lifecycle={event}"); + handle.dispatch_event(tag, event, &serde_json::json!({}), cx); + } + // A timer can complete after the display-frame chain has gone idle, so the + // JavaScript that handles these events needs a render to arrive in. + handle.wake(cx); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn activity_state_edges_emit_lifecycle_in_order() { + assert_eq!(lifecycle_events(0.0, 1.0), &["topWillAppear"]); + assert_eq!(lifecycle_events(1.0, 2.0), &["topAppear"]); + assert_eq!(lifecycle_events(2.0, 1.0), &["topWillDisappear"]); + assert_eq!(lifecycle_events(1.0, 0.0), &["topDisappear"]); + assert_eq!(lifecycle_events(0.0, 2.0), &["topWillAppear", "topAppear"]); + assert_eq!( + lifecycle_events(2.0, 0.0), + &["topWillDisappear", "topDisappear"] + ); + assert!(lifecycle_events(1.0, 1.0).is_empty()); + } +} diff --git a/vendor/react-native-gpui/modules/screens/desktop/src/props.rs b/vendor/react-native-gpui/modules/screens/desktop/src/props.rs new file mode 100644 index 0000000000..b8dbf124f4 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/src/props.rs @@ -0,0 +1,177 @@ +//! Typed readers over the JSON this module's C++ half produced. +//! +//! A missing key reads as today's zero value, so a component that has not been +//! given the prop yet behaves the way it did before it arrived. +use react_native_gpui::extensions::ComponentNode; + +/// A number, or 0 when absent. +fn number(node: &ComponentNode, key: &str) -> f64 { + node.props + .get(key) + .and_then(|value| value.as_f64()) + .unwrap_or(0.0) +} + +/// A number as `f32`, or 0 when absent. +fn number_f32(node: &ComponentNode, key: &str) -> f32 { + number(node, key) as f32 +} + +/// A string, borrowed from the props. +fn string<'a>(node: &'a ComponentNode, key: &str) -> &'a str { + node.props + .get(key) + .and_then(|value| value.as_str()) + .unwrap_or("") +} + +/// A boolean, false when absent. +fn boolean(node: &ComponentNode, key: &str) -> bool { + node.props + .get(key) + .and_then(|value| value.as_bool()) + .unwrap_or(false) +} + +/// A colour as 0xAARRGGBB, 0 when unset. +fn color(node: &ComponentNode, key: &str) -> u32 { + number(node, key) as u32 +} + +/// An `RNSScreen`'s activity state. 0 is detached, 1 is transitioning, 2 is +/// fully on screen. +pub fn activity_state(node: &ComponentNode) -> f32 { + number_f32(node, "activityState") +} + +/// An `RNSScreen`'s `stackAnimation`. +pub fn stack_animation(node: &ComponentNode) -> &str { + string(node, "stackAnimation") +} + +/// An `RNSScreen`'s `stackPresentation`. +pub fn stack_presentation(node: &ComponentNode) -> &str { + string(node, "stackPresentation") +} + +/// Whether a presentation paints its own card over the one underneath. +pub fn is_modal_presentation(node: &ComponentNode) -> bool { + matches!( + stack_presentation(node), + "modal" | "transparentModal" | "containedModal" | "formSheet" + ) +} + +/// Whether that card lets the screen underneath show through. +pub fn is_transparent_presentation(node: &ComponentNode) -> bool { + stack_presentation(node) == "transparentModal" +} + +/// A header config's title. +pub fn header_title(node: &ComponentNode) -> &str { + string(node, "title") +} + +/// A header config's background colour. +pub fn header_background_color(node: &ComponentNode) -> u32 { + color(node, "backgroundColor") +} + +/// A header config's title colour. +pub fn header_title_color(node: &ComponentNode) -> u32 { + color(node, "titleColor") +} + +/// A header config's tint colour, used for the back button. +pub fn header_tint_color(node: &ComponentNode) -> u32 { + color(node, "tintColor") +} + +/// Whether a header config hides the whole bar. +pub fn header_hidden(node: &ComponentNode) -> bool { + boolean(node, "hidden") +} + +/// Whether a header config hides only the back button. +pub fn header_hide_back(node: &ComponentNode) -> bool { + boolean(node, "hideBackButton") +} + +/// A header subview's placement: `left`, `right`, `center`, `title` or a Yoga x. +pub fn subview_type(node: &ComponentNode) -> &str { + string(node, "type") +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + fn node(json: serde_json::Value) -> ComponentNode { + ComponentNode { + component: "RNSScreen".to_string(), + tag: 1, + frame: rn_gpui_sys::ffi::RectF::default(), + scale: (1.0, 1.0), + props: Arc::new(json), + parent: None, + children: Vec::new(), + } + } + + #[test] + fn a_missing_key_reads_as_the_zero_value() { + let empty = node(serde_json::json!({})); + assert_eq!(activity_state(&empty), 0.0); + assert_eq!(stack_animation(&empty), ""); + assert_eq!(stack_presentation(&empty), ""); + assert_eq!(header_title(&empty), ""); + assert_eq!(header_background_color(&empty), 0); + assert!(!header_hidden(&empty)); + assert!(!header_hide_back(&empty)); + assert!(!is_modal_presentation(&empty)); + } + + #[test] + fn reads_the_fields_the_props_half_writes() { + let screen = node(serde_json::json!({ + "activityState": 2, + "stackAnimation": "slide_from_right", + "stackPresentation": "transparentModal", + })); + assert_eq!(activity_state(&screen), 2.0); + assert_eq!(stack_animation(&screen), "slide_from_right"); + assert!(is_modal_presentation(&screen)); + assert!(is_transparent_presentation(&screen)); + + let header = node(serde_json::json!({ + "title": "Home", + "backgroundColor": 0xff112233u32, + "titleColor": 0xffaabbccu32, + "tintColor": 0xff445566u32, + "hidden": true, + "hideBackButton": true, + })); + assert_eq!(header_title(&header), "Home"); + assert_eq!(header_background_color(&header), 0xff112233); + assert_eq!(header_title_color(&header), 0xffaabbcc); + assert_eq!(header_tint_color(&header), 0xff445566); + assert!(header_hidden(&header)); + assert!(header_hide_back(&header)); + + let subview = node(serde_json::json!({"type": "right"})); + assert_eq!(subview_type(&subview), "right"); + } + + #[test] + fn only_the_modal_presentations_count_as_modal() { + for presentation in ["modal", "transparentModal", "containedModal", "formSheet"] { + let node = node(serde_json::json!({"stackPresentation": presentation})); + assert!(is_modal_presentation(&node), "{presentation}"); + } + for presentation in ["push", "card", ""] { + let node = node(serde_json::json!({"stackPresentation": presentation})); + assert!(!is_modal_presentation(&node), "{presentation}"); + } + } +} diff --git a/vendor/react-native-gpui/modules/screens/desktop/src/render.rs b/vendor/react-native-gpui/modules/screens/desktop/src/render.rs new file mode 100644 index 0000000000..0bc0d1b2d8 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/src/render.rs @@ -0,0 +1,829 @@ +//! Painting react-native-screens through the generic renderer hooks. +//! +//! Core builds a box, a background and an accessibility node for every +//! component. What is screens-specific — which screen is on top, where a header +//! subview sits, whether a card is showing — is decided here, from the props this +//! module's C++ half produced and a read-only view of the tree. +use crate::props; +use crate::stack::{ + CONTENT_WRAPPER, FULL_WINDOW_OVERLAY, HEADER_CONFIG, HEADER_SUBVIEW, MODAL_SCREEN, SCREEN, + STACK, +}; +use gpui::{div, prelude::*, AnyElement, App, MouseButton, SharedString, Window}; +use react_native_gpui::extensions::{ + store_handle, ComponentNode, ComponentRenderer, Decoration, Portal, StoreHandle, TreeView, +}; +use rn_gpui_sys::ffi::RectF; + +/// The header bar's height, which the content wrapper is inset by. +pub const HEADER_HEIGHT: f32 = 56.0; + +/// A screen inside a stack. +pub struct ScreenRenderer; + +impl ComponentRenderer for ScreenRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn children( + &self, + node: &ComponentNode, + children: Vec, + _window: &mut Window, + _cx: &mut App, + ) -> Vec { + if !props::is_modal_presentation(node) { + return children; + } + let material = props::is_transparent_presentation(node); + // The frame is in unscaled coordinates and the element this returns is + // painted scaled, so the sheet has to be scaled to land where the card + // it covers does. The old code did the same with `scale_x`/`scale_y`. + let (scale_x, scale_y) = node.scale; + let width = (node.frame.w - SHEET_MARGIN).clamp(0.0, SHEET_MAX_WIDTH) * scale_x; + let height = (node.frame.h - SHEET_MARGIN).clamp(0.0, SHEET_MAX_HEIGHT) * scale_y; + let left = ((node.frame.w * scale_x - width) * 0.5).max(0.0); + let top = ((node.frame.h * scale_y - height) * 0.5).max(0.0); + let mut layers = Vec::with_capacity(2); + if !material { + layers.push( + div() + .absolute() + .inset_0() + .size_full() + .bg(argb(0x80000000)) + .into_any_element(), + ); + } + layers.push( + div() + .absolute() + .left(gpui::px(left)) + .top(gpui::px(top)) + .w(gpui::px(width)) + .h(gpui::px(height)) + .overflow_hidden() + .bg(argb(if material { 0x00000000 } else { 0xff171923 })) + .children(children) + .into_any_element(), + ); + layers + } + + fn hides_subtree(&self, node: &ComponentNode, tree: &TreeView) -> bool { + if props::activity_state(node) > 0.0 { + return false; + } + if tree + .transition(node.tag) + .is_some_and(|state| state.progress < 1.0) + { + return false; + } + !covered_by_modal_sibling(node, tree) + } + + fn decorate(&self, node: &ComponentNode, tree: &TreeView) -> Decoration { + if props::activity_state(node) > 0.0 { + return Decoration::default(); + } + let transitioning = tree + .transition(node.tag) + .is_some_and(|state| state.progress < 1.0); + let visible_under_modal = covered_by_modal_sibling(node, tree); + if !transitioning && !visible_under_modal { + return Decoration { + display: Some(false), + ..Decoration::default() + }; + } + Decoration::default() + } +} + +/// Whether a later sibling in the same stack is an active modal presentation. +fn covered_by_modal_sibling(node: &ComponentNode, tree: &TreeView) -> bool { + let Some(parent) = node.parent else { + return false; + }; + let siblings = tree.children(parent); + let Some(position) = siblings.iter().position(|tag| *tag == node.tag) else { + return false; + }; + siblings + .into_iter() + .skip(position + 1) + .filter_map(|tag| tree.node(tag)) + .any(|sibling| { + sibling.component == SCREEN + && props::activity_state(&sibling) > 0.0 + && props::is_modal_presentation(&sibling) + }) +} + +/// The stack itself: it clips, and it owns the Escape gesture. +pub struct StackRenderer; + +impl ComponentRenderer for StackRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn decorate(&self, _node: &ComponentNode, _tree: &TreeView) -> Decoration { + Decoration { + clip: true, + ..Decoration::default() + } + } + + fn handles_keys(&self, _node: &ComponentNode) -> bool { + true + } + + fn on_key_down( + &self, + node: &ComponentNode, + key: &str, + tree: &TreeView, + handle: &react_native_gpui::extensions::StoreHandle, + window: &mut Window, + cx: &mut App, + ) -> bool { + if key != "escape" { + return false; + } + if has_screen_ancestor(node, tree) { + return false; + } + let screens: Vec = tree + .children(node.tag) + .into_iter() + .filter(|tag| { + tree.is_live(*tag) && tree.node(*tag).is_some_and(|n| n.component == SCREEN) + }) + .collect(); + let Some(top) = screens.last().copied() else { + return false; + }; + if crate::state::with(|state| state.native_dismiss_pending.is_some()) { + return true; + } + dismiss_top(top, node.tag, tree, handle, window, cx); + true + } +} + +/// Whether any ancestor of `node` is a screen, i.e. this stack is nested. +fn has_screen_ancestor(node: &ComponentNode, tree: &TreeView) -> bool { + tree.ancestors(node.tag) + .into_iter() + .filter_map(|tag| tree.node(tag)) + .any(|ancestor| ancestor.component == SCREEN) +} + +/// The header bar: a background, a title and a back button. +pub struct HeaderConfigRenderer; + +impl ComponentRenderer for HeaderConfigRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn layout_frame(&self, node: &ComponentNode, tree: &TreeView) -> Option { + let mut frame = node.frame; + frame.y += tree.window_top_inset(); + frame.h = if props::header_hidden(node) || !is_top_header(node, tree) { + 0.0 + } else { + HEADER_HEIGHT + }; + Some(frame) + } + + fn hides_subtree(&self, node: &ComponentNode, tree: &TreeView) -> bool { + props::header_hidden(node) || !is_top_header(node, tree) + } + + fn decorate(&self, node: &ComponentNode, tree: &TreeView) -> Decoration { + let top = is_top_header(node, tree); + let shown = should_show_back(node, tree); + crate::state::set_header_facts( + node.tag, + top, + shown, + has_title_subview(node, tree), + node.parent, + ); + if props::header_hidden(node) || !top { + return Decoration { + display: Some(false), + ..Decoration::default() + }; + } + let background = first_nonzero(props::header_background_color(node), 0xff171923); + Decoration { + background: Some(background), + ..Decoration::default() + } + } + + fn depends_on_window_insets(&self) -> bool { + true + } + + fn content( + &self, + node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + if props::header_hidden(node) || !crate::state::header_is_top(node.tag) { + return None; + } + // React Native declares `onHeaderBackButtonClicked` on the screen, so + // the config's parent is the tag that has a listener for it. + let Some(screen_tag) = crate::state::header_screen(node.tag) else { + return None; + }; + let title_color = first_nonzero(props::header_title_color(node), 0xffffffff); + let mut bar = div() + .absolute() + .top_0() + .left_0() + .size_full() + .flex() + .items_center() + .justify_center() + .text_color(argb(title_color)) + .text_size(gpui::px(18.0)); + if !crate::state::header_has_title_subview(node.tag) { + bar = bar.child(SharedString::from(props::header_title(node).to_string())); + } + if crate::state::header_shows_back(node.tag) { + bar = bar.child( + div() + .absolute() + .left_0() + .top_0() + .h(gpui::px(HEADER_HEIGHT)) + .w(gpui::px(HEADER_HEIGHT)) + .flex() + .items_center() + .justify_center() + .text_color(argb(first_nonzero( + props::header_tint_color(node), + 0xffffffff, + ))) + .text_size(gpui::px(30.0)) + .cursor_pointer() + .child("‹") + .on_mouse_down(MouseButton::Left, move |_event, window, cx| { + if let Some(handle) = store_handle(cx) { + handle.dispatch_event( + screen_tag, + "headerBackButtonClicked", + &serde_json::json!({}), + cx, + ); + handle.request_frame(window); + } + cx.stop_propagation(); + }), + ); + } + Some(bar.into_any_element()) + } +} + +/// Escape dismisses the top screen the way a native gesture would. +/// +/// The card runs its exit animation, the screen gets the `will` pair, and only +/// when the animation ends does it get the final progress, the `did` pair and +/// `topDismissed`. `native_dismiss_pending` tells the stack observer that this +/// dismissal already ran its animation, so it must not run a second one. +fn dismiss_top( + top: i32, + stack: i32, + tree: &TreeView, + handle: &StoreHandle, + window: &mut Window, + cx: &mut App, +) { + crate::state::with(|state| state.native_dismiss_pending = Some(top)); + let incoming = previous_screen(top, stack, tree); + crate::lifecycle::dispatch_will(handle, Some(top), incoming, cx); + // The animation belongs to the screen being dismissed, not to the stack: a + // stack carries no `stackAnimation` prop of its own. A dismissal plays the + // entry mirrored, which is what `reverse` is for. + let animation = tree + .node(top) + .map(|node| props::stack_animation(&node).to_string()) + .unwrap_or_default(); + let mode = crate::transition::transition_mode(&animation) + .filter(|_| tree.animations_enabled()) + .map(crate::transition::TransitionMode::reverse); + let duration = mode.map(|mode| mode.duration()); + if let Some(mode) = mode { + let exit = crate::transition::transition(mode, crate::transition::Role::Exit, 1.0); + crate::state::with(|state| { + state.roles.insert(top, (crate::state::Role::Exit, 1.0)); + }); + handle.start_transition(top, exit, cx); + // The card underneath comes back to rest from wherever the push left + // it. Without this it stays offset for the whole dismissal. + if let Some(incoming) = incoming { + handle.clear_transition(incoming, cx); + } + handle.request_frame(window); + } + let finish = move |handle: &StoreHandle, cx: &mut App| { + handle.dispatch_event( + top, + "topTransitionProgress", + &serde_json::json!({"progress": 1, "closing": 1, "goingForward": 0}), + cx, + ); + crate::lifecycle::dispatch_did(handle, Some(top), incoming, cx); + handle.dispatch_event( + top, + "topDismissed", + &serde_json::json!({ "dismissCount": 1 }), + cx, + ); + }; + match duration { + Some(duration) => { + let handle = handle.clone(); + handle.after(duration, cx, move |handle, cx| finish(handle, cx)); + } + None => finish(handle, cx), + } +} + +/// A Fabric colour as GPUI sees it, alpha included. +fn argb(color: u32) -> gpui::Hsla { + let alpha = color >> 24; + gpui::rgba((color << 8) | alpha).into() +} + +fn first_nonzero(value: u32, fallback: u32) -> u32 { + if value == 0 { + fallback + } else { + value + } +} + +/// Whether the screen has a previous live screen in its stack to go back to. +fn should_show_back(node: &ComponentNode, tree: &TreeView) -> bool { + if props::header_hide_back(node) { + return false; + } + let Some(screen) = node.parent else { + return false; + }; + let Some(stack) = stack_of(screen, tree) else { + return false; + }; + let screens: Vec = tree + .children(stack) + .into_iter() + .filter(|tag| tree.is_live(*tag) && tree.node(*tag).is_some_and(|n| n.component == SCREEN)) + .collect(); + screens + .iter() + .position(|tag| *tag == screen) + .is_some_and(|position| position > 0) +} + +/// Whether this header belongs to the screen currently on top of its stack. +fn is_top_header(node: &ComponentNode, tree: &TreeView) -> bool { + let Some(parent) = node.parent else { + return true; + }; + let Some(stack) = stack_of(parent, tree) else { + return true; + }; + let screens: Vec = tree + .children(stack) + .into_iter() + .filter(|tag| tree.node(*tag).is_some_and(|n| n.component == SCREEN)) + .collect(); + let is_last = screens.last().copied() == Some(parent); + // A screen that is leaving keeps its header while its transition runs, so + // the header does not blink out before the card has finished moving. An + // entry transition is never cleared once it ends, so only a running one + // counts: otherwise the first screen ever pushed keeps its header for good. + is_last + || tree + .transition(parent) + .is_some_and(|state| state.progress < 1.0) +} + +/// Whether the header config carries a subview that paints the title. +/// +/// React Navigation puts a custom title in a `title` subview, and a centred one +/// in a `center` subview; either way that subview paints the title, so core must +/// not paint a second copy. +fn has_title_subview(config: &ComponentNode, tree: &TreeView) -> bool { + tree.children(config.tag) + .into_iter() + .filter_map(|tag| tree.node(tag)) + .any(|child| { + child.component == HEADER_SUBVIEW + && matches!(props::subview_type(&child), "title" | "center") + }) +} + +/// The live screen directly below `top` in its stack, if any. +fn previous_screen(top: i32, stack: i32, tree: &TreeView) -> Option { + let screens: Vec = tree + .children(stack) + .into_iter() + .filter(|tag| tree.is_live(*tag) && tree.node(*tag).is_some_and(|n| n.component == SCREEN)) + .collect(); + let index = screens.iter().position(|tag| *tag == top)?; + index.checked_sub(1).and_then(|i| screens.get(i)).copied() +} + +/// The stack a screen sits in, walking up past intermediate containers. +fn stack_of(screen: i32, tree: &TreeView) -> Option { + tree.ancestors(screen) + .into_iter() + .find(|tag| tree.node(*tag).is_some_and(|n| n.component == STACK)) +} + +/// A header subview: positioned by its declared type. +pub struct HeaderSubviewRenderer; + +impl ComponentRenderer for HeaderSubviewRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn layout_frame(&self, node: &ComponentNode, tree: &TreeView) -> Option { + let mut frame = node.frame; + let Some(config) = node.parent else { + return Some(frame); + }; + let container_width = tree + .node(config) + .map(|config| config.frame.w) + .unwrap_or(frame.w); + let tallest = tree + .children(node.tag) + .into_iter() + .filter_map(|tag| tree.node(tag)) + .fold(0.0f32, |height, child| height.max(child.frame.h)); + frame.x = match props::subview_type(node) { + "right" => container_width - frame.w - 12.0, + "center" | "title" => (container_width - frame.w) / 2.0, + "left" => 12.0, + _ => frame.x, + }; + frame.y = ((HEADER_HEIGHT - tallest).max(0.0)) / 2.0; + Some(frame) + } +} + +/// A screen's content area, inset by the header when one is showing. +pub struct ContentWrapperRenderer; + +impl ComponentRenderer for ContentWrapperRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn layout_frame(&self, node: &ComponentNode, tree: &TreeView) -> Option { + let Some(screen) = node.parent else { + return None; + }; + let mut frame = node.frame; + // Only a header that is actually showing takes space. When it is hidden + // the C++ half already gave the wrapper the whole screen, so the Yoga + // frame is the answer and any adjustment here would move it twice. + if !header_hidden(screen, tree) { + frame.y += HEADER_HEIGHT; + frame.h = (frame.h - HEADER_HEIGHT).max(0.0); + } + Some(frame) + } +} + +/// Whether a screen's header config is hidden, whether or not it exists. +fn header_hidden(screen: i32, tree: &TreeView) -> bool { + tree.children(screen) + .into_iter() + .filter_map(|tag| tree.node(tag)) + .find(|node| node.component == HEADER_CONFIG) + .is_some_and(|config| props::header_hidden(&config)) +} + +/// The gap a modal sheet leaves around itself, and the size it is capped at. +const SHEET_MARGIN: f32 = 48.0; +const SHEET_MAX_WIDTH: f32 = 760.0; +const SHEET_MAX_HEIGHT: f32 = 640.0; + +/// A modal presentation. It paints like a screen; the difference is that the +/// card underneath stays visible. +pub struct ModalScreenRenderer; + +impl ComponentRenderer for ModalScreenRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } +} + +/// A full-window overlay paints over the whole window, not inside the tree. +pub struct FullWindowOverlayRenderer; + +impl ComponentRenderer for FullWindowOverlayRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn portal(&self, _props: &serde_json::Value) -> Option { + Some(Portal { + visible: true, + close_request_event: None, + }) + } +} + +/// Every component name this module paints, and how to build its renderer. +pub const RENDERED_COMPONENTS: &[(&str, fn() -> Box)] = &[ + (STACK, || Box::new(StackRenderer)), + (SCREEN, || Box::new(ScreenRenderer)), + (MODAL_SCREEN, || Box::new(ModalScreenRenderer)), + (HEADER_CONFIG, || Box::new(HeaderConfigRenderer)), + (HEADER_SUBVIEW, || Box::new(HeaderSubviewRenderer)), + (CONTENT_WRAPPER, || Box::new(ContentWrapperRenderer)), + (FULL_WINDOW_OVERLAY, || Box::new(FullWindowOverlayRenderer)), +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::state; + use std::sync::Arc; + + /// Builds a node with the props this module's C++ half would have produced. + fn node(component: &str, tag: i32, props: serde_json::Value) -> ComponentNode { + ComponentNode { + component: component.to_string(), + tag, + frame: RectF::default(), + scale: (1.0, 1.0), + props: Arc::new(props), + parent: None, + children: Vec::new(), + } + } + + fn frame(x: f32, y: f32, w: f32, h: f32) -> RectF { + RectF { x, y, w, h } + } + + /// A tree built from plain values, matching what core would hand a hook. + fn tree( + nodes: Vec, + edges: &[(i32, i32)], + top_inset: f32, + ) -> TreeView<'static> { + TreeView::for_test(nodes, edges, top_inset) + } + + fn entry( + tag: i32, + component: &'static str, + frame: RectF, + props: serde_json::Value, + ) -> react_native_gpui::extensions::TestNode { + react_native_gpui::extensions::TestNode { + tag, + component: component.to_string(), + frame, + scale: (1.0, 1.0), + props, + } + } + + /// A stack with one screen, its header config and its content wrapper. + /// + /// `header` is the header config's props and `content_height` the wrapper's + /// Yoga height, which the C++ half computes for the whole screen. + fn stack_with_header( + header: serde_json::Value, + content_height: f32, + ) -> (TreeView<'static>, i32, i32) { + let nodes = vec![ + entry( + 1, + STACK, + frame(0.0, 0.0, 400.0, 600.0), + serde_json::json!({}), + ), + entry( + 2, + SCREEN, + frame(0.0, 0.0, 400.0, 600.0), + serde_json::json!({"activityState": 2, "stackPresentation": "card"}), + ), + entry(3, HEADER_CONFIG, frame(0.0, 0.0, 400.0, 56.0), header), + entry( + 4, + CONTENT_WRAPPER, + frame(0.0, 0.0, 400.0, content_height), + serde_json::json!({}), + ), + ]; + let edges = [(1, 2), (2, 3), (2, 4)]; + (tree(nodes, &edges, 0.0), 3, 4) + } + + #[test] + fn a_visible_header_pushes_the_content_down_by_its_height() { + // Yoga gives the wrapper the whole screen; a visible header takes the + // top 56 px of it, so the content starts below the bar. + let (tree, _, wrapper) = stack_with_header(serde_json::json!({"title": "Home"}), 600.0); + let node = tree.node(wrapper).expect("wrapper is mounted"); + let laid_out = ContentWrapperRenderer + .layout_frame(&node, &tree) + .expect("the wrapper is laid out"); + assert_eq!(laid_out.y, HEADER_HEIGHT); + assert_eq!(laid_out.h, 600.0 - HEADER_HEIGHT); + } + + #[test] + fn a_hidden_header_leaves_the_content_where_yoga_put_it() { + // The C++ half gives a hidden-header screen's wrapper the full height, + // so any adjustment here would move the content a second time: the + // reported bug was content sitting 56 px above the top of the window. + let (tree, _, wrapper) = + stack_with_header(serde_json::json!({"title": "Home", "hidden": true}), 600.0); + let node = tree.node(wrapper).expect("wrapper is mounted"); + let laid_out = ContentWrapperRenderer + .layout_frame(&node, &tree) + .expect("the wrapper is laid out"); + assert_eq!(laid_out.y, 0.0); + assert_eq!(laid_out.h, 600.0); + } + + #[test] + fn the_header_sits_below_the_window_inset_and_is_one_bar_tall() { + let (tree, header, _) = stack_with_header(serde_json::json!({"title": "Home"}), 600.0); + // A window whose title bar takes 28 px: the bar must start below it. + let tree = tree_with_inset(&tree, 28.0); + let node = tree.node(header).expect("header is mounted"); + let laid_out = HeaderConfigRenderer + .layout_frame(&node, &tree) + .expect("the header is laid out"); + assert_eq!(laid_out.y, 28.0); + assert_eq!(laid_out.h, HEADER_HEIGHT); + } + + #[test] + fn a_hidden_header_takes_no_height_and_paints_nothing() { + let (tree, header, _) = + stack_with_header(serde_json::json!({"title": "Home", "hidden": true}), 600.0); + let node = tree.node(header).expect("header is mounted"); + let laid_out = HeaderConfigRenderer + .layout_frame(&node, &tree) + .expect("the header is laid out"); + assert_eq!(laid_out.h, 0.0); + assert!(HeaderConfigRenderer.hides_subtree(&node, &tree)); + assert_eq!( + HeaderConfigRenderer.decorate(&node, &tree).display, + Some(false) + ); + } + + /// A header carrying a `title` or `center` subview must not paint a second + /// title of its own: the subview is the title. + #[test] + fn a_title_subview_suppresses_the_headers_own_title() { + for subview_type in ["title", "center"] { + let nodes = vec![ + entry( + 1, + STACK, + frame(0.0, 0.0, 400.0, 600.0), + serde_json::json!({}), + ), + entry( + 2, + SCREEN, + frame(0.0, 0.0, 400.0, 600.0), + serde_json::json!({"activityState": 2, "stackPresentation": "card"}), + ), + entry( + 3, + HEADER_CONFIG, + frame(0.0, 0.0, 400.0, 56.0), + serde_json::json!({"title": "Details 1"}), + ), + entry( + 4, + HEADER_SUBVIEW, + frame(0.0, 0.0, 80.0, 20.0), + serde_json::json!({ "type": subview_type }), + ), + ]; + let edges = [(1, 2), (2, 3), (3, 4)]; + let tree = tree(nodes, &edges, 0.0); + let config = tree.node(3).expect("header config is mounted"); + assert!( + has_title_subview(&config, &tree), + "a {subview_type} subview paints the title" + ); + } + } + + #[test] + fn a_header_without_a_title_subview_paints_its_title() { + let (tree, header, _) = stack_with_header(serde_json::json!({"title": "Home"}), 600.0); + let node = tree.node(header).expect("header is mounted"); + assert!(!has_title_subview(&node, &tree)); + } + + #[test] + fn the_header_paints_the_colours_react_native_asked_for() { + let (tree, header, _) = stack_with_header( + serde_json::json!({ + "title": "Home", + "backgroundColor": 0xff203040u32, + }), + 600.0, + ); + let node = tree.node(header).expect("header is mounted"); + let decoration = HeaderConfigRenderer.decorate(&node, &tree); + assert_eq!(decoration.background, Some(0xff203040)); + assert_ne!(decoration.display, Some(false)); + } + + #[test] + fn a_header_without_a_background_falls_back_to_the_default() { + let (tree, header, _) = stack_with_header(serde_json::json!({"title": "Home"}), 600.0); + let node = tree.node(header).expect("header is mounted"); + assert_eq!( + HeaderConfigRenderer.decorate(&node, &tree).background, + Some(0xff171923) + ); + } + + /// The same tree as `stack_with_header` but with a window inset. + fn tree_with_inset(source: &TreeView<'_>, inset: f32) -> TreeView<'static> { + let mut nodes: Vec = Vec::new(); + let mut edges = Vec::new(); + for tag in [1, 2, 3, 4] { + let node = source.node(tag).expect("node is mounted"); + nodes.push(react_native_gpui::extensions::TestNode { + tag, + component: node.component, + frame: node.frame, + scale: node.scale, + props: (*node.props).clone(), + }); + if let Some(parent) = node.parent { + edges.push((parent, tag)); + } + } + tree(nodes, &edges, inset) + } +} diff --git a/vendor/react-native-gpui/modules/screens/desktop/src/stack.rs b/vendor/react-native-gpui/modules/screens/desktop/src/stack.rs new file mode 100644 index 0000000000..8e76165ef4 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/src/stack.rs @@ -0,0 +1,396 @@ +//! The stack observer: everything react-native-screens does between a mount +//! batch and a paint. +//! +//! Core tells an observer about a batch before it applies it, about each insert +//! and removal as it applies them, and about the batch once it is done. All the +//! navigation decisions — which stack is nested, which card is leaving, when a +//! lifecycle event fires, when a card is kept alive for its exit — are made here +//! and remembered in `state`. +use crate::props; +use crate::state::{self, PopPlan, Role}; +use crate::transition::{self, Role as TransitionRole}; +use react_native_gpui::extensions::{ + BatchOpKind, BatchView, ComponentNode, StoreHandle, StoreObserver, TreeView, +}; +use std::collections::HashSet; + +/// The component names this module owns. +pub const STACK: &str = "RNSScreenStack"; +pub const SCREEN: &str = "RNSScreen"; +pub const HEADER_CONFIG: &str = "RNSScreenStackHeaderConfig"; +pub const HEADER_SUBVIEW: &str = "RNSScreenStackHeaderSubview"; +pub const CONTENT_WRAPPER: &str = "RNSScreenContentWrapper"; +pub const MODAL_SCREEN: &str = "RNSModalScreen"; +pub const FULL_WINDOW_OVERLAY: &str = "RNSFullWindowOverlay"; + +/// Watches the batch and drives the stack. +pub struct StackObserver; + +impl StoreObserver for StackObserver { + fn before_batch( + &self, + batch: &BatchView, + tree: &TreeView, + handle: &StoreHandle, + cx: &mut gpui::App, + ) { + let nested = nested_stacks(batch, tree); + if std::env::var_os("RN_GPUI_DEBUG_SCREENS").is_some() && !nested.is_empty() { + log::info!("[screens] nested stack tags={nested:?}"); + } + let plans = pop_plans(batch, tree, &nested, handle, cx); + for (tag, plan) in &plans { + if plan.retain { + handle.retain_subtree(*tag); + } + } + state::with(|state| { + state.nested_stacks = nested; + state.pop_plans = plans; + }); + } + + fn on_mount(&self, node: &ComponentNode, _handle: &StoreHandle, _cx: &mut gpui::App) { + if node.component == FULL_WINDOW_OVERLAY { + log::info!( + "[screens] full window overlay created tag={} frame={}x{}", + node.tag, + node.frame.w, + node.frame.h + ); + } else if crate::unsupported::unsupported_component(&node.component) { + log::error!( + "[screens] component {} is not implemented on gpui", + node.component + ); + } + } + + fn on_insert( + &self, + child: &ComponentNode, + parent: i32, + previous_children: &[i32], + tree: &TreeView, + handle: &StoreHandle, + cx: &mut gpui::App, + ) { + if child.component != SCREEN || !is_outer_stack(parent) { + return; + } + let animation = props::stack_animation(child).to_string(); + let presentation = props::stack_presentation(child).to_string(); + log::info!( + "[screens] screen inserted tag={} animation={} presentation={} reduce_motion={}", + child.tag, + animation, + presentation, + !tree.animations_enabled() + ); + let previous_top = previous_children + .iter() + .rev() + .copied() + .find(|tag| tree.node(*tag).is_some_and(|node| node.component == SCREEN)); + let mode = transition::transition_mode(&animation).filter(|_| tree.animations_enabled()); + if let Some(mode) = mode { + log::info!( + "[screens] push transition incoming={} outgoing={:?} animation={} mode={mode:?}", + child.tag, + previous_top, + animation + ); + if presentation == "push" { + if let Some(previous) = previous_top { + let distance = transition::PARALLAX_DISTANCE; + handle.start_transition( + previous, + transition::transition(mode, TransitionRole::Exit, distance), + cx, + ); + state::with(|state| { + state.roles.insert(previous, (Role::Exit, distance)); + }); + } + } + handle.start_transition( + child.tag, + transition::transition(mode, TransitionRole::Enter, 1.0), + cx, + ); + state::with(|state| { + state.roles.insert(child.tag, (Role::Enter, 1.0)); + }); + } + crate::lifecycle::dispatch_will(handle, previous_top, Some(child.tag), cx); + match mode { + Some(mode) => { + let outgoing = previous_top; + let incoming = Some(child.tag); + let handle = handle.clone(); + handle.after(mode.duration(), cx, move |handle, cx| { + if let Some(outgoing) = outgoing { + handle.clear_transition(outgoing, cx); + } + crate::lifecycle::dispatch_did(handle, outgoing, incoming, cx); + }); + } + None => crate::lifecycle::dispatch_did(handle, previous_top, Some(child.tag), cx), + } + } + + fn on_remove_child( + &self, + child: i32, + parent: i32, + retained: bool, + tree: &TreeView, + handle: &StoreHandle, + cx: &mut gpui::App, + ) { + let Some(node) = tree.node(child) else { + return; + }; + if node.component != SCREEN { + return; + } + let plan = state::with(|state| state.pop_plans.remove(&child)); + let Some(plan) = plan else { + return; + }; + let _ = parent; + if let Some(revealed) = plan.revealed { + handle.clear_transition(revealed, cx); + } + let native_dismiss = state::with(|state| state.native_dismiss_pending == Some(child)); + let mode = transition::transition_mode(&plan.animation) + .filter(|_| tree.animations_enabled()) + .map(transition::TransitionMode::reverse); + let retain = plan.retain && mode.is_some(); + log::info!( + "[screens] pop transition outgoing={} incoming={:?} animation={} retained={retain}", + child, + plan.revealed, + plan.animation + ); + if retain { + if let Some(mode) = mode { + handle.start_transition( + child, + transition::transition(mode, TransitionRole::Exit, 1.0), + cx, + ); + state::with(|state| { + state.roles.insert(child, (Role::Exit, 1.0)); + }); + } + } + if !native_dismiss { + crate::lifecycle::dispatch_will(handle, Some(child), plan.revealed, cx); + } + if retained { + let duration = mode.map(|mode| mode.duration()).unwrap_or_default(); + let revealed = plan.revealed; + let handle = handle.clone(); + handle.after(duration, cx, move |handle, cx| { + handle.release_retained(child, cx); + log::info!("[screens] pop finished outgoing={child}"); + crate::lifecycle::dispatch_did(handle, Some(child), revealed, cx); + }); + } else if !native_dismiss { + crate::lifecycle::dispatch_did(handle, Some(child), plan.revealed, cx); + } + state::with(|state| { + if state.native_dismiss_pending == Some(child) { + state.native_dismiss_pending = None; + } + }); + } + + fn on_remove(&self, tag: i32, _handle: &StoreHandle, _cx: &mut gpui::App) { + state::with(|state| { + state.pop_plans.remove(&tag); + state.roles.remove(&tag); + state.headers.remove(&tag); + }); + } + + fn on_update( + &self, + node: &ComponentNode, + previous: &ComponentNode, + handle: &StoreHandle, + cx: &mut gpui::App, + ) { + if node.component != SCREEN { + return; + } + let before = props::activity_state(previous); + let after = props::activity_state(node); + if before == after { + return; + } + for event in crate::lifecycle::lifecycle_events(before, after) { + log::info!( + "[screens] tag={} activity={}->{} event={event}", + node.tag, + before, + after + ); + handle.dispatch_event(node.tag, event, &serde_json::json!({}), cx); + } + } + + fn after_batch(&self, _tree: &TreeView, _handle: &StoreHandle, _cx: &mut gpui::App) { + state::clear_batch_state(); + } + + fn on_transition_frame( + &self, + tag: i32, + progress: f32, + handle: &StoreHandle, + cx: &mut gpui::App, + ) { + let forward = state::with(|state| { + state + .roles + .get(&tag) + .is_none_or(|(role, distance)| *role == Role::Enter || *distance < 1.0) + }); + handle.dispatch_event( + tag, + "topTransitionProgress", + &serde_json::json!({ + "progress": progress, + "closing": if forward { 0 } else { 1 }, + "goingForward": if forward { 1 } else { 0 } + }), + cx, + ); + } +} + +/// Whether `stack` is the outer stack, the only one that animates. +fn is_outer_stack(stack: i32) -> bool { + !state::with(|state| state.nested_stacks.contains(&stack)) +} + +/// Every stack that sits inside a screen, found from the batch topology before +/// any of it is applied. +fn nested_stacks(batch: &BatchView, tree: &TreeView) -> HashSet { + let stacks: Vec = batch + .ops + .iter() + .filter(|op| op.kind == BatchOpKind::Create && op.component == STACK) + .map(|op| op.tag) + .chain( + batch + .ops + .iter() + .filter(|op| op.kind != BatchOpKind::Create && op.component == STACK) + .map(|op| op.tag), + ) + .collect(); + let mut nested = HashSet::new(); + for stack in stacks { + let mut cursor = Some(stack); + let mut visited = HashSet::new(); + while let Some(tag) = cursor { + if !visited.insert(tag) { + break; + } + let Some(parent) = batch.parent_of(tag).or_else(|| tree.parent(tag)) else { + break; + }; + if batch.component_of(parent, tree).as_deref() == Some(SCREEN) { + nested.insert(stack); + break; + } + cursor = Some(parent); + } + } + nested +} + +/// The cards leaving in this batch that should animate their exit. +fn pop_plans( + batch: &BatchView, + tree: &TreeView, + nested: &HashSet, + handle: &StoreHandle, + cx: &mut gpui::App, +) -> std::collections::HashMap { + let removed: HashSet = batch + .ops + .iter() + .filter(|op| op.kind == BatchOpKind::Remove && op.component == SCREEN) + .map(|op| op.tag) + .collect(); + let mut plans = std::collections::HashMap::new(); + for op in batch + .ops + .iter() + .filter(|op| op.kind == BatchOpKind::Remove && op.component == SCREEN) + { + if nested.contains(&op.parent_tag) { + continue; + } + let Some(stack) = tree.node(op.parent_tag) else { + continue; + }; + if stack.component != STACK { + continue; + } + let live: Vec = tree + .children(op.parent_tag) + .into_iter() + .filter(|tag| { + tree.is_live(*tag) && tree.node(*tag).is_some_and(|node| node.component == SCREEN) + }) + .collect(); + if live.last().copied() != Some(op.tag) { + continue; + } + let revealed = live + .iter() + .rev() + .find(|tag| !removed.contains(tag)) + .copied(); + let Some(screen) = tree.node(op.tag) else { + continue; + }; + let animation = props::stack_animation(&screen).to_string(); + let native_dismiss = state::with(|state| state.native_dismiss_pending == Some(op.tag)); + let mode = transition::transition_mode(&animation).filter(|_| tree.animations_enabled()); + let retain = !native_dismiss && revealed.is_some() && mode.is_some(); + if retain { + log::info!( + "[screens] preserve outgoing={} descendants={}", + op.tag, + descendant_count(op.tag, tree) + ); + } + plans.insert( + op.tag, + PopPlan { + revealed, + animation, + duration: mode.map(|mode| mode.duration()).unwrap_or_default(), + retain, + }, + ); + } + plans +} + +/// How many nodes sit below `tag`, at any depth. +fn descendant_count(tag: i32, tree: &TreeView) -> usize { + let mut total = 0; + let mut stack = tree.children(tag); + while let Some(next) = stack.pop() { + total += 1; + stack.extend(tree.children(next)); + } + total +} diff --git a/vendor/react-native-gpui/modules/screens/desktop/src/state.rs b/vendor/react-native-gpui/modules/screens/desktop/src/state.rs new file mode 100644 index 0000000000..9bf4ce4720 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/src/state.rs @@ -0,0 +1,162 @@ +//! The module's own state, kept outside core. +//! +//! Core hands an observer facts about a batch and forgets them; everything a +//! stack has to remember between batches — which stacks are nested, which cards +//! are leaving, what a dismissal already animated — is here. +use std::collections::{HashMap, HashSet}; +use std::sync::{Mutex, OnceLock}; +use std::time::Duration; + +/// What `decorate` learned about a header, for `content` to use. +#[derive(Clone, Copy, Debug, Default)] +pub struct HeaderFacts { + pub is_top: bool, + pub shows_back: bool, + pub has_title_subview: bool, + /// The screen this header belongs to, which is where a back-button event + /// has to go: React Native declares `onHeaderBackButtonClicked` on + /// `RNSScreen`, not on the header config. + pub screen: Option, +} + +/// Records what `decorate` learned, so `content` can read it. +pub fn set_header_facts( + tag: i32, + is_top: bool, + shows_back: bool, + has_title_subview: bool, + screen: Option, +) { + with(|state| { + state.headers.insert( + tag, + HeaderFacts { + is_top, + shows_back, + has_title_subview, + screen, + }, + ); + }); +} + +/// Whether this header is the top screen's, as of the last `decorate` call. +pub fn header_is_top(tag: i32) -> bool { + with(|state| state.headers.get(&tag).is_some_and(|facts| facts.is_top)) +} + +/// Whether this header carries a `title` subview, as of the last `decorate` call. +/// +/// When it does, that subview paints the title and core must not paint its own. +pub fn header_has_title_subview(tag: i32) -> bool { + with(|state| { + state + .headers + .get(&tag) + .is_some_and(|facts| facts.has_title_subview) + }) +} + +/// The screen a header belongs to, as of the last `decorate` call. +pub fn header_screen(tag: i32) -> Option { + with(|state| state.headers.get(&tag).and_then(|facts| facts.screen)) +} + +/// Whether this header shows a back button, as of the last `decorate` call. +pub fn header_shows_back(tag: i32) -> bool { + with(|state| { + state + .headers + .get(&tag) + .is_some_and(|facts| facts.shows_back) + }) +} + +/// A card that is leaving, and what should happen when it is gone. +#[derive(Clone, Debug)] +pub struct PopPlan { + /// The screen that becomes top when this one is gone. + pub revealed: Option, + /// The animation name React Native asked for, unparsed. + pub animation: String, + /// How long the exit runs. + pub duration: Duration, + /// Whether the card must stay attached so its exit can paint. + pub retain: bool, +} + +/// Everything the module remembers between batches. +#[derive(Default)] +pub struct State { + /// A native dismissal already ran the exit animation, so the removal must + /// not run a second one and must not send the "will" events again. + pub native_dismiss_pending: Option, + /// Stacks that sit inside a screen, so only the outer stack animates. + pub nested_stacks: HashSet, + /// Cards leaving in this batch, by tag. + pub pop_plans: HashMap, + /// The cards this module started a transition on: whether it is an entry + /// (the screen is arriving) or an exit, and how far it travels. + pub roles: HashMap, + /// The latest facts about each header, written by `decorate`. + pub headers: HashMap, +} + +/// Whether a transition the module started is an arrival or a departure. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Role { + Enter, + Exit, +} + +fn state() -> &'static Mutex { + static STATE: OnceLock> = OnceLock::new(); + STATE.get_or_init(|| Mutex::new(State::default())) +} + +/// Runs `f` with the module state. +pub fn with(f: impl FnOnce(&mut State) -> R) -> R { + let mut state = state() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + f(&mut state) +} + +/// Forgets the per-batch decisions. Call from `after_batch`. +pub fn clear_batch_state() { + with(|state| { + state.pop_plans.clear(); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn batch_state_is_cleared_but_lifetimes_are_not() { + with(|state| { + state.pop_plans.insert( + 7, + PopPlan { + revealed: Some(3), + animation: "slide_from_right".to_string(), + duration: Duration::from_millis(240), + retain: true, + }, + ); + state.nested_stacks.insert(9); + state.native_dismiss_pending = Some(7); + }); + clear_batch_state(); + with(|state| { + assert!(state.pop_plans.is_empty()); + assert!(state.nested_stacks.contains(&9)); + assert_eq!(state.native_dismiss_pending, Some(7)); + }); + with(|state| { + state.nested_stacks.clear(); + state.native_dismiss_pending = None; + }); + } +} diff --git a/vendor/react-native-gpui/modules/screens/desktop/src/transition.rs b/vendor/react-native-gpui/modules/screens/desktop/src/transition.rs new file mode 100644 index 0000000000..e94f7fdd3d --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/src/transition.rs @@ -0,0 +1,193 @@ +//! The animation modes a stack accepts, and the poses each one corresponds to. +//! +//! Core owns the timing and the paint; this file is the module's own statement +//! of what "slide from the right" and "fade" mean as movement between two +//! poses, so core never learns a navigation name. +use react_native_gpui::extensions::{Pose, Transition}; +use std::time::Duration; + +/// A stack animation, as `stackAnimation` names it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TransitionMode { + Fade, + SlideFromRight, + SlideFromLeft, +} + +impl TransitionMode { + /// The mode the reverse of this one uses, so a pop plays the entry mirrored. + #[must_use] + pub fn reverse(self) -> Self { + match self { + Self::SlideFromRight => Self::SlideFromLeft, + Self::SlideFromLeft => Self::SlideFromRight, + Self::Fade => Self::Fade, + } + } + + /// How long this mode runs. + pub fn duration(self) -> Duration { + match self { + Self::Fade => Duration::from_millis(150), + _ => Duration::from_millis(240), + } + } +} + +/// The mode a `stackAnimation` string asks for. Unknown values fade, which is +/// what the platform's own default is. +pub fn transition_mode(animation: &str) -> Option { + match animation { + "none" => None, + "slide_from_right" | "simple_push" => Some(TransitionMode::SlideFromRight), + "slide_from_left" => Some(TransitionMode::SlideFromLeft), + "default" | "" | "fade" => Some(TransitionMode::Fade), + _ => Some(TransitionMode::Fade), + } +} + +/// Whether entering or leaving. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Role { + Enter, + Exit, +} + +/// The poses a mode and role correspond to. +/// +/// Entering slides from off-screen to rest; leaving slides from rest to +/// off-screen; a fade keeps the node still while its opacity moves. The offset +/// is a fraction of the node's own width, so `distance` is how far it travels. +pub fn poses(mode: TransitionMode, role: Role, distance: f32) -> (Pose, Pose) { + let rest = Pose { + offset_x: 0.0, + opacity: 1.0, + }; + let transparent = Pose { + offset_x: 0.0, + opacity: 0.0, + }; + let off_screen = |sign: f32| Pose { + offset_x: sign * distance, + opacity: 1.0, + }; + match (mode, role) { + (TransitionMode::Fade, Role::Enter) => (transparent, rest), + (TransitionMode::Fade, Role::Exit) => (rest, transparent), + (TransitionMode::SlideFromRight, Role::Enter) => (off_screen(1.0), rest), + (TransitionMode::SlideFromLeft, Role::Enter) => (off_screen(-1.0), rest), + (TransitionMode::SlideFromRight, Role::Exit) => (rest, off_screen(-1.0)), + (TransitionMode::SlideFromLeft, Role::Exit) => (rest, off_screen(1.0)), + } +} + +/// The transition a mode and role describe. +pub fn transition(mode: TransitionMode, role: Role, distance: f32) -> Transition { + let (from, to) = poses(mode, role, distance); + Transition { + from, + to, + duration: mode.duration(), + } +} + +/// The card underneath a push parallaxes a short way rather than leaving. +pub const PARALLAX_DISTANCE: f32 = 0.12; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_supported_animations_and_falls_back_to_fade() { + assert_eq!(transition_mode("none"), None); + assert_eq!(transition_mode("fade"), Some(TransitionMode::Fade)); + assert_eq!(transition_mode("default"), Some(TransitionMode::Fade)); + assert_eq!(transition_mode(""), Some(TransitionMode::Fade)); + assert_eq!( + transition_mode("slide_from_right"), + Some(TransitionMode::SlideFromRight) + ); + assert_eq!( + transition_mode("simple_push"), + Some(TransitionMode::SlideFromRight) + ); + assert_eq!( + transition_mode("slide_from_left"), + Some(TransitionMode::SlideFromLeft) + ); + assert_eq!(transition_mode("unsupported"), Some(TransitionMode::Fade)); + } + + #[test] + fn reversing_a_slide_swaps_its_direction() { + assert_eq!( + TransitionMode::SlideFromRight.reverse(), + TransitionMode::SlideFromLeft + ); + assert_eq!( + TransitionMode::SlideFromLeft.reverse(), + TransitionMode::SlideFromRight + ); + assert_eq!(TransitionMode::Fade.reverse(), TransitionMode::Fade); + } + + #[test] + fn the_poses_reproduce_the_original_slide_and_fade_curves() { + for (mode, role, distance, expected) in [ + ( + TransitionMode::SlideFromRight, + Role::Enter, + 1.0, + [1.0, 0.5, 0.0], + ), + ( + TransitionMode::SlideFromLeft, + Role::Enter, + 1.0, + [-1.0, -0.5, 0.0], + ), + ( + TransitionMode::SlideFromRight, + Role::Exit, + 1.0, + [0.0, -0.5, -1.0], + ), + ( + TransitionMode::SlideFromLeft, + Role::Exit, + 1.0, + [0.0, 0.5, 1.0], + ), + ( + TransitionMode::SlideFromRight, + Role::Exit, + PARALLAX_DISTANCE, + [0.0, -0.06, -0.12], + ), + ] { + let transition = transition(mode, role, distance); + for (progress, expected_offset) in + [(0.0, expected[0]), (0.5, expected[1]), (1.0, expected[2])] + { + let pose = transition.pose_at(progress); + assert!( + (pose.offset_x - expected_offset).abs() < 0.001, + "{mode:?} {role:?} at {progress}: {} != {expected_offset}", + pose.offset_x + ); + } + } + } + + #[test] + fn a_fade_moves_opacity_and_nothing_else() { + let transition = transition(TransitionMode::Fade, Role::Enter, 1.0); + for (progress, expected) in [(0.0, 0.0), (0.5, 0.5), (1.0, 1.0)] { + let pose = transition.pose_at(progress); + assert!((pose.opacity - expected).abs() < 0.001); + assert!(pose.offset_x.abs() < 0.001); + } + assert_eq!(transition.duration, Duration::from_millis(150)); + } +} diff --git a/vendor/react-native-gpui/modules/screens/desktop/src/unsupported.rs b/vendor/react-native-gpui/modules/screens/desktop/src/unsupported.rs new file mode 100644 index 0000000000..4222c9ef0a --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/desktop/src/unsupported.rs @@ -0,0 +1,39 @@ +//! Components react-native-screens ships that this platform does not implement. +//! +//! They are named here so the module can say so once, instead of core carrying a +//! list of a library's components. +/// Whether this component is one the platform cannot render. +pub fn unsupported_component(name: &str) -> bool { + matches!( + name, + "RNSSearchBar" + | "RNSBottomTabs" + | "RNSBottomTabsScreen" + | "RNSScreenFooter" + | "RNSSplitView" + | "RNSSplitViewScreen" + ) || name.starts_with("RNSSplitView") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn identifies_the_components_the_platform_cannot_render() { + for name in [ + "RNSSearchBar", + "RNSBottomTabs", + "RNSBottomTabsScreen", + "RNSScreenFooter", + "RNSSplitView", + "RNSSplitViewScreen", + ] { + assert!(unsupported_component(name), "{name} should be unsupported"); + } + assert!(unsupported_component("RNSSplitViewSomethingElse")); + for name in ["RNSScreen", "RNSScreenStack", "View", "RNSScreenContainer"] { + assert!(!unsupported_component(name), "{name} should be supported"); + } + } +} diff --git a/vendor/react-native-gpui/modules/screens/package.json b/vendor/react-native-gpui/modules/screens/package.json new file mode 100644 index 0000000000..2c415809f9 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/package.json @@ -0,0 +1,15 @@ +{ + "name": "@react-native-gpui/screens", + "version": "0.1.0", + "description": "GPUI platform integration for react-native-screens", + "files": [ + "desktop", + "react-native.config.js", + "src" + ], + "peerDependencies": { + "react-native": "0.87.1", + "react-native-screens": "4.28.x" + }, + "license": "MIT" +} diff --git a/vendor/react-native-gpui/modules/screens/react-native.config.js b/vendor/react-native-gpui/modules/screens/react-native.config.js new file mode 100644 index 0000000000..6f949d7fe8 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/react-native.config.js @@ -0,0 +1,36 @@ +module.exports = { + dependency: { + platforms: { + desktop: { + cmake: 'desktop', + crate: 'rn-gpui-screens', + componentCodegenPackage: 'react-native-screens', + componentDescriptors: [ + 'RNSScreenComponentDescriptor', + 'RNSScreenContainerComponentDescriptor', + 'RNSScreenContentWrapperComponentDescriptor', + 'RNSScreenNavigationContainerComponentDescriptor', + 'RNSScreenStackComponentDescriptor', + 'RNSScreenStackHeaderConfigComponentDescriptor', + 'RNSScreenStackHeaderSubviewComponentDescriptor', + 'RNSModalScreenComponentDescriptor', + 'RNSFullWindowOverlayComponentDescriptor', + ], + componentHeaders: [ + 'react/renderer/components/rnscreens/ComponentDescriptors.h', + 'react/renderer/components/rnscreens/RNSScreenComponentDescriptor.h', + 'react/renderer/components/rnscreens/RNSScreenStackHeaderConfigComponentDescriptor.h', + 'react/renderer/components/rnscreens/RNSScreenStackHeaderSubviewComponentDescriptor.h', + 'react/renderer/components/rnscreens/RNSModalScreenComponentDescriptor.h', + 'react/renderer/components/rnscreens/RNSFullWindowOverlayComponentDescriptor.h', + ], + // Metro: replace these files of installed packages for gpui. + sourceOverrides: { + 'react-native-screens/src/core.ts': './src/core.desktop.ts', + 'react-native-screens/src/components/FullWindowOverlay.tsx': + './src/FullWindowOverlay.desktop.tsx', + }, + }, + }, + }, +}; diff --git a/vendor/react-native-gpui/modules/screens/src/FullWindowOverlay.desktop.tsx b/vendor/react-native-gpui/modules/screens/src/FullWindowOverlay.desktop.tsx new file mode 100644 index 0000000000..761bcc383e --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/src/FullWindowOverlay.desktop.tsx @@ -0,0 +1,19 @@ +import React, { type PropsWithChildren } from 'react'; +import { StyleSheet, useWindowDimensions } from 'react-native'; +import FullWindowOverlayNativeComponent from 'react-native-screens/src/fabric/FullWindowOverlayNativeComponent'; +import type { NativeProps } from 'react-native-screens/src/fabric/FullWindowOverlayNativeComponent'; + +export default function FullWindowOverlay({ + children, + ...props +}: PropsWithChildren) { + const { width, height } = useWindowDimensions(); + return ( + + {children} + + ); +} diff --git a/vendor/react-native-gpui/modules/screens/src/core.desktop.ts b/vendor/react-native-gpui/modules/screens/src/core.desktop.ts new file mode 100644 index 0000000000..9a5e1a98e7 --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/src/core.desktop.ts @@ -0,0 +1,28 @@ +import './customDirectEventTypes.desktop'; + +// GPUI always ships the native screens implementation. +export const isNativePlatformSupported = true; + +let ENABLE_SCREENS = isNativePlatformSupported; + +export function enableScreens(shouldEnableScreens = true) { + ENABLE_SCREENS = shouldEnableScreens; +} + +let ENABLE_FREEZE = false; + +export function enableFreeze(shouldEnableReactFreeze = true) { + if (!isNativePlatformSupported) { + return; + } + + ENABLE_FREEZE = shouldEnableReactFreeze; +} + +export function screensEnabled() { + return ENABLE_SCREENS; +} + +export function freezeEnabled() { + return ENABLE_FREEZE; +} diff --git a/vendor/react-native-gpui/modules/screens/src/customDirectEventTypes.desktop.ts b/vendor/react-native-gpui/modules/screens/src/customDirectEventTypes.desktop.ts new file mode 100644 index 0000000000..39f81385fd --- /dev/null +++ b/vendor/react-native-gpui/modules/screens/src/customDirectEventTypes.desktop.ts @@ -0,0 +1,16 @@ +// The native stack header dispatches this upstream screen event to notify +// React Navigation that the user requested a native back action. +import { customDirectEventTypes } from 'react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry'; + +customDirectEventTypes.topHeaderBackButtonClicked = { + registrationName: 'onHeaderBackButtonClicked', +}; + +customDirectEventTypes.topWillAppear = { registrationName: 'onWillAppear' }; +customDirectEventTypes.topWillDisappear = { + registrationName: 'onWillDisappear', +}; +customDirectEventTypes.topAppear = { registrationName: 'onAppear' }; +customDirectEventTypes.topDisappear = { registrationName: 'onDisappear' }; + +export { customDirectEventTypes }; diff --git a/vendor/react-native-gpui/modules/svg/desktop/CMakeLists.txt b/vendor/react-native-gpui/modules/svg/desktop/CMakeLists.txt new file mode 100644 index 0000000000..0eb1aa2dc8 --- /dev/null +++ b/vendor/react-native-gpui/modules/svg/desktop/CMakeLists.txt @@ -0,0 +1,8 @@ +# SVG shared Fabric props/shadow-node descriptors are supplied by RN Codegen +# and react-native-svg's published common C++ shadow-node headers. +set(_svg_codegen_dir "${RN_GPUI_PROJECT_ROOT}/desktop/autolink/codegen/rnsvg") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/cpp/GpuiSvgModule.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INSTALLERS svg) +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INCLUDE_DIRS + "${_svg_codegen_dir}") diff --git a/vendor/react-native-gpui/modules/svg/desktop/Cargo.toml b/vendor/react-native-gpui/modules/svg/desktop/Cargo.toml new file mode 100644 index 0000000000..d2199cba04 --- /dev/null +++ b/vendor/react-native-gpui/modules/svg/desktop/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rn-gpui-svg" +version = "0.1.0" +edition = "2021" + +[dependencies] +react-native-gpui = { version = "0.1" } +rn-gpui-sys = { version = "0.1" } +gpui = { version = "0.2" } +serde_json = "1" diff --git a/vendor/react-native-gpui/modules/svg/desktop/build.rs b/vendor/react-native-gpui/modules/svg/desktop/build.rs new file mode 100644 index 0000000000..d18a3120d0 --- /dev/null +++ b/vendor/react-native-gpui/modules/svg/desktop/build.rs @@ -0,0 +1,38 @@ +//! Puts the C++ runtime's shared libraries on this crate's test binary rpath. +//! +//! `rn-gpui-svg`'s tests build gpui trees, which link `libjsi.dylib` and the rest +//! of the Node-API runtime. Core's `build.rs` emits +//! `cargo:rustc-link-arg=-Wl,-rpath,` for its own binary — the app — and that +//! flag is not inherited by a dependency's test binary, so the test process +//! aborted in dyld with `Library not loaded: @rpath/libjsi.dylib` before running a +//! single test. +//! +//! Core exports the directories it was given as `DEP_RN_GPUI_CORE_RPATHS`, which +//! is the same variable core's own `build.rs` reads. Re-emitting it here scopes +//! the flag to this crate's test binary, since `cargo:rustc-link-arg` without a +//! target suffix applies to the crate's own targets and not to the app that +//! depends on it. + +fn main() { + println!("cargo:rerun-if-env-changed=DEP_RN_GPUI_CORE_RPATHS"); + println!("cargo:rerun-if-changed=build.rs"); + + let Ok(dirs) = std::env::var("DEP_RN_GPUI_CORE_RPATHS") else { + return; + }; + let dirs = dirs + .split('|') + .filter(|dir| !dir.is_empty()) + .collect::>(); + if dirs.is_empty() { + return; + } + // MSVC and MinGW have no rpath, and the flag is rejected outright; Windows + // finds the DLLs through PATH, which the test runner already arranges. + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + return; + } + for dir in dirs { + println!("cargo:rustc-link-arg=-Wl,-rpath,{dir}"); + } +} diff --git a/vendor/react-native-gpui/modules/svg/desktop/cpp/GpuiSvgModule.cpp b/vendor/react-native-gpui/modules/svg/desktop/cpp/GpuiSvgModule.cpp new file mode 100644 index 0000000000..63b4367bd5 --- /dev/null +++ b/vendor/react-native-gpui/modules/svg/desktop/cpp/GpuiSvgModule.cpp @@ -0,0 +1,141 @@ +#include "GpuiModules.h" +#include "MountBatchBuilder.h" + +#include + +#include +#include +#include + +namespace rngpui { +using namespace facebook::react; + +namespace { +constexpr const char* kSvgComponents[] = { + "RNSVGSvgView", + "RNSVGCircle", + "RNSVGEllipse", + "RNSVGLine", + "RNSVGPath", + "RNSVGRect", + "RNSVGGroup", + "RNSVGDefs", + "RNSVGClipPath", + "RNSVGLinearGradient", + "RNSVGRadialGradient", + "RNSVGText", + "RNSVGTSpan", +}; + +template +folly::dynamic svgJsonValue(const T& value) { + return value; +} + +folly::dynamic svgJsonValue(const Float& value) { + return value; +} +folly::dynamic svgJsonValue(const SharedColor& value) { + return value ? folly::dynamic(argb(value)) : folly::dynamic(nullptr); +} +folly::dynamic svgJsonValue(const std::vector& values) { + folly::dynamic result = folly::dynamic::array(); + for (const auto& value : values) + result.push_back(value); + return result; +} +folly::dynamic svgJsonValue(const std::vector& values) { + folly::dynamic result = folly::dynamic::array(); + for (auto value : values) + result.push_back(value); + return result; +} + +template +folly::dynamic svgPropsJson(const T& props) { + folly::dynamic result = folly::dynamic::object(); +#define RN_GPUI_COPY_SVG_PROP(name) \ + if constexpr (requires { props.name; }) \ + result[#name] = svgJsonValue(props.name) + RN_GPUI_COPY_SVG_PROP(name); + RN_GPUI_COPY_SVG_PROP(d); + RN_GPUI_COPY_SVG_PROP(x); + RN_GPUI_COPY_SVG_PROP(y); + RN_GPUI_COPY_SVG_PROP(x1); + RN_GPUI_COPY_SVG_PROP(y1); + RN_GPUI_COPY_SVG_PROP(x2); + RN_GPUI_COPY_SVG_PROP(y2); + RN_GPUI_COPY_SVG_PROP(cx); + RN_GPUI_COPY_SVG_PROP(cy); + RN_GPUI_COPY_SVG_PROP(r); + RN_GPUI_COPY_SVG_PROP(rx); + RN_GPUI_COPY_SVG_PROP(ry); + RN_GPUI_COPY_SVG_PROP(width); + RN_GPUI_COPY_SVG_PROP(height); + RN_GPUI_COPY_SVG_PROP(vbWidth); + RN_GPUI_COPY_SVG_PROP(vbHeight); + RN_GPUI_COPY_SVG_PROP(minX); + RN_GPUI_COPY_SVG_PROP(minY); + RN_GPUI_COPY_SVG_PROP(align); + RN_GPUI_COPY_SVG_PROP(meetOrSlice); + RN_GPUI_COPY_SVG_PROP(opacity); + RN_GPUI_COPY_SVG_PROP(color); + RN_GPUI_COPY_SVG_PROP(propList); + RN_GPUI_COPY_SVG_PROP(fill); + RN_GPUI_COPY_SVG_PROP(fillOpacity); + RN_GPUI_COPY_SVG_PROP(fillRule); + RN_GPUI_COPY_SVG_PROP(stroke); + RN_GPUI_COPY_SVG_PROP(strokeOpacity); + RN_GPUI_COPY_SVG_PROP(strokeWidth); + RN_GPUI_COPY_SVG_PROP(strokeLinecap); + RN_GPUI_COPY_SVG_PROP(strokeLinejoin); + RN_GPUI_COPY_SVG_PROP(strokeDasharray); + RN_GPUI_COPY_SVG_PROP(strokeDashoffset); + RN_GPUI_COPY_SVG_PROP(strokeMiterlimit); + RN_GPUI_COPY_SVG_PROP(matrix); + RN_GPUI_COPY_SVG_PROP(mask); + RN_GPUI_COPY_SVG_PROP(clipPath); + RN_GPUI_COPY_SVG_PROP(offset); + RN_GPUI_COPY_SVG_PROP(stopColor); + RN_GPUI_COPY_SVG_PROP(stopOpacity); +#undef RN_GPUI_COPY_SVG_PROP + return result; +} + +folly::dynamic svgPropsJson(const std::string& component, const Props& props) { +#define RN_GPUI_SVG_PROPS(tag, type) \ + if (component == tag) { \ + if (auto typed = dynamic_cast(&props)) \ + return svgPropsJson(*typed); \ + } + RN_GPUI_SVG_PROPS("RNSVGSvgView", RNSVGSvgViewProps) + RN_GPUI_SVG_PROPS("RNSVGCircle", RNSVGCircleProps) + RN_GPUI_SVG_PROPS("RNSVGEllipse", RNSVGEllipseProps) + RN_GPUI_SVG_PROPS("RNSVGLine", RNSVGLineProps) + RN_GPUI_SVG_PROPS("RNSVGPath", RNSVGPathProps) + RN_GPUI_SVG_PROPS("RNSVGRect", RNSVGRectProps) + RN_GPUI_SVG_PROPS("RNSVGGroup", RNSVGGroupProps) + RN_GPUI_SVG_PROPS("RNSVGDefs", RNSVGDefsProps) + RN_GPUI_SVG_PROPS("RNSVGClipPath", RNSVGClipPathProps) + RN_GPUI_SVG_PROPS("RNSVGLinearGradient", RNSVGLinearGradientProps) + RN_GPUI_SVG_PROPS("RNSVGRadialGradient", RNSVGRadialGradientProps) + RN_GPUI_SVG_PROPS("RNSVGText", RNSVGTextProps) + RN_GPUI_SVG_PROPS("RNSVGTSpan", RNSVGTSpanProps) +#undef RN_GPUI_SVG_PROPS + return folly::dynamic::object(); +} + +} // namespace + +void rn_gpui_install_svg_module() { + for (const char* component : kSvgComponents) { + registerComponentProps( + component, [](const std::string& name, const ShadowView& view) { + if (!view.props) + return std::string("{}"); + return folly::toJson(svgPropsJson(name, *view.props)); + }); + } +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/modules/svg/desktop/src/lib.rs b/vendor/react-native-gpui/modules/svg/desktop/src/lib.rs new file mode 100644 index 0000000000..31b88a64a1 --- /dev/null +++ b/vendor/react-native-gpui/modules/svg/desktop/src/lib.rs @@ -0,0 +1,465 @@ +use gpui::{div, prelude::*, px, AnyElement, App, Window}; +use react_native_gpui::extensions::{ + register_component_renderer, ComponentNode, ComponentRenderer, +}; +use rn_gpui_sys::ffi::RectF; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::sync::{Arc, Mutex, OnceLock, Weak}; + +const COMPONENTS: &[&str] = &[ + "RNSVGSvgView", + "RNSVGCircle", + "RNSVGEllipse", + "RNSVGLine", + "RNSVGPath", + "RNSVGRect", + "RNSVGGroup", + "RNSVGDefs", + "RNSVGClipPath", + "RNSVGLinearGradient", + "RNSVGRadialGradient", + "RNSVGText", + "RNSVGTSpan", + "RNSVGMask", + "RNSVGSymbol", + "RNSVGPattern", + "RNSVGMarker", + "RNSVGTextPath", + "RNSVGUse", +]; + +struct SvgRenderer; + +impl ComponentRenderer for SvgRenderer { + fn render( + &self, + node: &ComponentNode, + window: &mut Window, + cx: &mut App, + ) -> Option { + if node.component != "RNSVGSvgView" { + return Some(div().into_any_element()); + } + // The frame is in unscaled coordinates and this element is painted + // scaled, exactly as `ComponentRenderer::render` documents, so the box + // has to be scaled the same way core scales the element it would have + // built. `size_full()` sized the image to its parent instead: the + // document is rasterized at the window's scale factor, and an image + // carries no intrinsic size, so every SVG stretched to fill whatever + // surrounded it and painted over the text below it. + let frame = node.frame; + let (scale_x, scale_y) = node.scale; + let width = frame.w * scale_x; + let height = frame.h * scale_y; + let document = svg_document(node, frame, cx); + let scale = window.scale_factor(); + let image = cached_svg_image(document, frame.w, frame.h, scale); + Some( + gpui::img(image) + .w(px(width)) + .h(px(height)) + .into_any_element(), + ) + } +} + +pub fn rn_gpui_register() { + for component in COMPONENTS { + register_component_renderer(*component, Arc::new(SvgRenderer)); + } +} + +fn svg_document(node: &ComponentNode, frame: RectF, cx: &App) -> String { + let props = &node.props; + let mut attributes = format!( + "xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\"", + svg_number(frame.w), + svg_number(frame.h) + ); + for (name, attribute) in [ + ("color", "color"), + ("fill", "fill"), + ("stroke", "stroke"), + ("strokeWidth", "stroke-width"), + ("fillOpacity", "fill-opacity"), + ("strokeOpacity", "stroke-opacity"), + ("opacity", "opacity"), + ] { + if let Some(value) = props.get(name).filter(|value| !value.is_null()) { + if let Some(value) = svg_value(value) { + attributes.push_str(&format!(" {attribute}=\"{}\"", svg_escape(&value))); + } + } + } + if let Some(cap) = props + .get("strokeLinecap") + .and_then(serde_json::Value::as_i64) + { + let cap = ["butt", "round", "square"] + .get(cap as usize) + .copied() + .unwrap_or("butt"); + attributes.push_str(&format!(" stroke-linecap=\"{cap}\"")); + } + if let Some(join) = props + .get("strokeLinejoin") + .and_then(serde_json::Value::as_i64) + { + let join = ["miter", "round", "bevel"] + .get(join as usize) + .copied() + .unwrap_or("miter"); + attributes.push_str(&format!(" stroke-linejoin=\"{join}\"")); + } + if let (Some(width), Some(height)) = ( + props.get("vbWidth").and_then(serde_json::Value::as_f64), + props.get("vbHeight").and_then(serde_json::Value::as_f64), + ) { + if width > 0.0 && height > 0.0 { + let min_x = props + .get("minX") + .and_then(serde_json::Value::as_f64) + .unwrap_or(0.0); + let min_y = props + .get("minY") + .and_then(serde_json::Value::as_f64) + .unwrap_or(0.0); + attributes.push_str(&format!( + " viewBox=\"{} {} {} {}\"", + svg_number(min_x as f32), + svg_number(min_y as f32), + svg_number(width as f32), + svg_number(height as f32) + )); + if let Some(align) = props.get("align").and_then(serde_json::Value::as_str) { + let mode = match props + .get("meetOrSlice") + .and_then(serde_json::Value::as_i64) + .unwrap_or(0) + { + 1 => "slice", + 2 => "none", + _ => "meet", + }; + attributes.push_str(&format!( + " preserveAspectRatio=\"{} {}\"", + svg_escape(align), + mode + )); + } + } + } + let mut document = format!(""); + for child in &node.children { + svg_node(child, cx, &mut document); + } + document.push_str(""); + document +} + +fn svg_node(node: &ComponentNode, cx: &App, output: &mut String) { + let props = &node.props; + let element = match node.component.as_str() { + "RNSVGCircle" => "circle", + "RNSVGEllipse" => "ellipse", + "RNSVGLine" => "line", + "RNSVGPath" => "path", + "RNSVGRect" => "rect", + "RNSVGGroup" => "g", + "RNSVGDefs" => "defs", + "RNSVGClipPath" => "clipPath", + "RNSVGLinearGradient" => "linearGradient", + "RNSVGRadialGradient" => "radialGradient", + "RNSVGMask" => "mask", + "RNSVGSymbol" => "symbol", + "RNSVGPattern" => "pattern", + "RNSVGMarker" => "marker", + "RNSVGText" => "text", + "RNSVGTSpan" => "tspan", + "RNSVGTextPath" => "textPath", + "RNSVGUse" => "use", + _ => "", + }; + if element.is_empty() { + for child in &node.children { + svg_node(child, cx, output); + } + return; + } + let mut attributes = String::new(); + for name in [ + "id", + "name", + "href", + "d", + "x", + "y", + "x1", + "y1", + "x2", + "y2", + "cx", + "cy", + "r", + "rx", + "ry", + "width", + "height", + "color", + "fill", + "stroke", + "strokeWidth", + "fillOpacity", + "strokeOpacity", + "strokeDasharray", + "strokeDashoffset", + "strokeMiterlimit", + "opacity", + "fontSize", + "fontWeight", + "fontFamily", + "textLength", + "dominantBaseline", + "offset", + "stopColor", + "stopOpacity", + ] { + if !svg_property_is_set(props, name) { + continue; + } + if let Some(value) = props.get(name) { + let attribute = match name { + "strokeWidth" => "stroke-width", + "fillOpacity" => "fill-opacity", + "strokeOpacity" => "stroke-opacity", + "strokeDasharray" => "stroke-dasharray", + "strokeDashoffset" => "stroke-dashoffset", + "strokeMiterlimit" => "stroke-miterlimit", + "fontSize" => "font-size", + "fontWeight" => "font-weight", + "fontFamily" => "font-family", + "dominantBaseline" => "dominant-baseline", + "stopColor" => "stop-color", + "stopOpacity" => "stop-opacity", + other => other, + }; + let value = if value.is_null() && matches!(name, "fill" | "stroke") { + Some("none".to_owned()) + } else { + svg_value(value) + }; + if let Some(value) = value { + attributes.push_str(&format!(" {attribute}=\"{}\"", svg_escape(&value))); + } + } + } + if let Some(rule) = props + .get("fillRule") + .filter(|_| svg_property_is_set(props, "fillRule")) + .and_then(serde_json::Value::as_i64) + { + attributes.push_str(&format!( + " fill-rule=\"{}\"", + if rule == 0 { "evenodd" } else { "nonzero" } + )); + } + if let Some(cap) = props + .get("strokeLinecap") + .filter(|_| svg_property_is_set(props, "strokeLinecap")) + .and_then(serde_json::Value::as_i64) + { + let cap = ["butt", "round", "square"] + .get(cap as usize) + .copied() + .unwrap_or("butt"); + attributes.push_str(&format!(" stroke-linecap=\"{cap}\"")); + } + if let Some(join) = props + .get("strokeLinejoin") + .filter(|_| svg_property_is_set(props, "strokeLinejoin")) + .and_then(serde_json::Value::as_i64) + { + let join = ["miter", "round", "bevel"] + .get(join as usize) + .copied() + .unwrap_or("miter"); + attributes.push_str(&format!(" stroke-linejoin=\"{join}\"")); + } + if let Some(matrix) = props.get("matrix").and_then(serde_json::Value::as_array) { + if matrix.len() >= 16 { + let values = [0, 1, 4, 5, 12, 13].map(|index| matrix[index].as_f64().unwrap_or(0.0)); + attributes.push_str(&format!( + " transform=\"matrix({} {} {} {} {} {})\"", + values[0], values[1], values[2], values[3], values[4], values[5] + )); + } + } + output.push('<'); + output.push_str(element); + output.push_str(&attributes); + output.push('>'); + for child in &node.children { + svg_node(child, cx, output); + } + output.push_str("'); +} + +fn svg_property_is_set(props: &serde_json::Value, name: &str) -> bool { + if !matches!( + name, + "fill" + | "fillOpacity" + | "fillRule" + | "stroke" + | "strokeWidth" + | "strokeOpacity" + | "strokeLinecap" + | "strokeLinejoin" + | "strokeDasharray" + | "strokeDashoffset" + | "strokeMiterlimit" + ) { + return true; + } + props + .get("propList") + .and_then(serde_json::Value::as_array) + .is_none_or(|properties| { + properties + .iter() + .any(|property| property.as_str() == Some(name)) + }) +} + +fn svg_value(value: &serde_json::Value) -> Option { + match value.get("type").and_then(serde_json::Value::as_i64) { + Some(1) => { + return value + .get("brushRef") + .and_then(serde_json::Value::as_str) + .map(|id| format!("url(#{id})")) + } + Some(2) => return Some("currentColor".to_owned()), + Some(3) => return Some("context-fill".to_owned()), + Some(4) => return Some("context-stroke".to_owned()), + _ => {} + } + + if let Some(number) = value.as_f64() { + if number < 0.0 && number.fract() == 0.0 { + return Some(format!("#{:06x}", (number as i64 as u32) & 0x00ff_ffff)); + } + if (0x0100_0000..=0xffff_ffff).contains(&(number as u64)) { + let argb = number as u32; + return Some(format!( + "rgba({}, {}, {}, {})", + (argb >> 16) & 0xff, + (argb >> 8) & 0xff, + argb & 0xff, + ((argb >> 24) & 0xff) as f32 / 255.0 + )); + } + return Some(svg_number(number as f32)); + } + if let Some(value) = value.as_str() { + return Some(value.to_owned()); + } + if let Some(value) = value.as_array() { + return Some( + value + .iter() + .filter_map(svg_value) + .collect::>() + .join(" "), + ); + } + value + .get("payload") + .and_then(svg_value) + .or_else(|| value.get("value").and_then(svg_value)) +} + +fn svg_escape(value: &str) -> String { + value + .replace('&', "&") + .replace('"', """) + .replace('<', "<") + .replace('>', ">") +} + +fn svg_number(value: f32) -> String { + if value.fract() == 0.0 { + format!("{value:.0}") + } else { + value.to_string() + } +} + +fn cached_svg_image( + document: String, + width: f32, + height: f32, + scale: f32, +) -> std::sync::Arc { + type SvgCache = HashMap<(u64, u32, u32, u32), Weak>; + static CACHE: OnceLock> = OnceLock::new(); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + document.hash(&mut hasher); + let key = ( + hasher.finish(), + width.to_bits(), + height.to_bits(), + scale.to_bits(), + ); + let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); + let mut cache = cache + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Some(image) = cache.get(&key).and_then(Weak::upgrade) { + return image; + } + let image = std::sync::Arc::new(gpui::Image::from_bytes( + gpui::ImageFormat::Svg, + document.into_bytes(), + )); + cache.insert(key, std::sync::Arc::downgrade(&image)); + if cache.len() > 512 { + cache.retain(|_, image| image.strong_count() > 0); + } + image +} + +#[cfg(test)] +mod svg_paint_tests { + use super::{svg_property_is_set, svg_value}; + use serde_json::json; + + #[test] + fn brushes_keep_current_color_and_reference_semantics() { + assert_eq!( + svg_value(&json!({"type": 2})).as_deref(), + Some("currentColor") + ); + assert_eq!( + svg_value(&json!({"type": 1, "brushRef": "gradient"})).as_deref(), + Some("url(#gradient)") + ); + assert_eq!( + svg_value(&json!({"type": 0, "payload": 0xff64d2ff_u32})).as_deref(), + Some("rgba(100, 210, 255, 1)") + ); + } + + #[test] + fn default_black_fill_does_not_override_inherited_none() { + let child = json!({"fill": {"type": 0, "payload": 0xff000000_u32}, "strokeLinecap": 0, "propList": []}); + assert!(!svg_property_is_set(&child, "fill")); + assert!(!svg_property_is_set(&child, "strokeLinecap")); + let group = json!({"fill": null, "stroke": {"type": 2}, "propList": ["fill", "stroke"]}); + assert!(svg_property_is_set(&group, "fill")); + assert!(svg_property_is_set(&group, "stroke")); + } +} diff --git a/vendor/react-native-gpui/modules/svg/package.json b/vendor/react-native-gpui/modules/svg/package.json new file mode 100644 index 0000000000..c6fc9efb6a --- /dev/null +++ b/vendor/react-native-gpui/modules/svg/package.json @@ -0,0 +1,14 @@ +{ + "name": "@react-native-gpui/svg", + "version": "0.1.0", + "description": "GPUI Fabric renderer adapter for react-native-svg", + "files": [ + "desktop", + "react-native.config.js" + ], + "peerDependencies": { + "react-native": "0.87.1", + "react-native-svg": "15.15.x" + }, + "license": "MIT" +} diff --git a/vendor/react-native-gpui/modules/svg/react-native.config.js b/vendor/react-native-gpui/modules/svg/react-native.config.js new file mode 100644 index 0000000000..88a9d4d06e --- /dev/null +++ b/vendor/react-native-gpui/modules/svg/react-native.config.js @@ -0,0 +1,51 @@ +module.exports = { + dependency: { + platforms: { + desktop: { + cmake: 'desktop', + crate: 'rn-gpui-svg', + componentCodegenPackage: 'react-native-svg', + componentSourcePackage: 'react-native-svg', + componentDescriptors: [ + 'RNSVGCircleComponentDescriptor', + 'RNSVGClipPathComponentDescriptor', + 'RNSVGDefsComponentDescriptor', + 'RNSVGFeBlendComponentDescriptor', + 'RNSVGFeColorMatrixComponentDescriptor', + 'RNSVGFeCompositeComponentDescriptor', + 'RNSVGFeFloodComponentDescriptor', + 'RNSVGFeGaussianBlurComponentDescriptor', + 'RNSVGFeMergeComponentDescriptor', + 'RNSVGFeOffsetComponentDescriptor', + 'RNSVGFilterComponentDescriptor', + 'RNSVGEllipseComponentDescriptor', + 'RNSVGForeignObjectComponentDescriptor', + 'RNSVGGroupComponentDescriptor', + 'RNSVGLinearGradientComponentDescriptor', + 'RNSVGLineComponentDescriptor', + 'RNSVGMarkerComponentDescriptor', + 'RNSVGMaskComponentDescriptor', + 'RNSVGPathComponentDescriptor', + 'RNSVGPatternComponentDescriptor', + 'RNSVGRadialGradientComponentDescriptor', + 'RNSVGRectComponentDescriptor', + 'RNSVGSymbolComponentDescriptor', + 'RNSVGTextComponentDescriptor', + 'RNSVGTextPathComponentDescriptor', + 'RNSVGTSpanComponentDescriptor', + 'RNSVGUseComponentDescriptor', + 'RNSVGSvgViewComponentDescriptor', + ], + componentHeaders: [ + 'react/renderer/components/rnsvg/ComponentDescriptors.h', + 'react/renderer/components/rnsvg/RNSVGComponentDescriptors.h', + ], + componentIncludeDirs: ['common/cpp'], + componentSources: [ + 'common/cpp/react/renderer/components/rnsvg/RNSVGShadowNodes.cpp', + 'common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp', + ], + }, + }, + }, +}; diff --git a/vendor/react-native-gpui/modules/worklets/desktop/CMakeLists.txt b/vendor/react-native-gpui/modules/worklets/desktop/CMakeLists.txt new file mode 100644 index 0000000000..b3d96448d0 --- /dev/null +++ b/vendor/react-native-gpui/modules/worklets/desktop/CMakeLists.txt @@ -0,0 +1,35 @@ +if(NOT RN_GPUI_PROJECT_ROOT) + message(FATAL_ERROR + "@react-native-gpui/worklets was added by the platform; RN_GPUI_PROJECT_ROOT must be set by its build.rs") +endif() +set(_gpui_app_root "${RN_GPUI_PROJECT_ROOT}") +find_program(_gpui_node node REQUIRED) +execute_process( + COMMAND "${_gpui_node}" -e "process.stdout.write(require.resolve('react-native-worklets/package.json', {paths: [process.argv[1]]}))" "${_gpui_app_root}" + RESULT_VARIABLE _worklets_resolve_result + OUTPUT_VARIABLE _worklets_package_json + OUTPUT_STRIP_TRAILING_WHITESPACE) +if(NOT _worklets_resolve_result EQUAL 0 OR NOT EXISTS "${_worklets_package_json}") + message(FATAL_ERROR "react-native-worklets could not be resolved from ${_gpui_app_root}") +endif() +get_filename_component(_worklets_root "${_worklets_package_json}" DIRECTORY) +set(_worklets_common "${_worklets_root}/Common/cpp") +if(NOT EXISTS "${_worklets_common}/worklets/NativeModules/WorkletsModuleProxy.h") + message(FATAL_ERROR "react-native-worklets 0.13.x was not found in ${_gpui_app_root} or any ancestor node_modules directory") +endif() + +file(GLOB_RECURSE _worklets_sources CONFIGURE_DEPENDS "${_worklets_common}/worklets/*.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES + ${_worklets_sources} + "${CMAKE_CURRENT_LIST_DIR}/cpp/GPUIWorkletsModule.cpp" + "${CMAKE_CURRENT_LIST_DIR}/cpp/GPUIPlatformLogger.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS + "${_worklets_common}" + "${_worklets_root}") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_DEFINITIONS + "WORKLETS_VERSION=0.13.0") +# The frame hook is how worklets see every frame; the platform compiles this +# glue only because the package is installed. +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/cpp/GpuiWorkletsFrameModule.cpp") +set_property(GLOBAL APPEND PROPERTY RN_GPUI_PACKAGE_MODULE_INSTALLERS worklets) diff --git a/vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIPlatformLogger.cpp b/vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIPlatformLogger.cpp new file mode 100644 index 0000000000..72c3bd8797 --- /dev/null +++ b/vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIPlatformLogger.cpp @@ -0,0 +1,27 @@ +#include + +#include + +namespace worklets { + +void PlatformLogger::log(const char* str) { + LOG(INFO) << str; +} + +void PlatformLogger::log(const std::string& str) { + LOG(INFO) << str; +} + +void PlatformLogger::log(const double value) { + LOG(INFO) << value; +} + +void PlatformLogger::log(const int value) { + LOG(INFO) << value; +} + +void PlatformLogger::log(const bool value) { + LOG(INFO) << (value ? "true" : "false"); +} + +} // namespace worklets diff --git a/vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIWorkletsModule.cpp b/vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIWorkletsModule.cpp new file mode 100644 index 0000000000..d06cce6221 --- /dev/null +++ b/vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIWorkletsModule.cpp @@ -0,0 +1,336 @@ +#include "GPUIWorkletsModule.h" + +#include "rn-gpui-sys/src/lib.rs.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "GpuiModules.h" + +namespace { +bool animationTracingEnabled() { + static const bool enabled = std::getenv("RN_GPUI_DEBUG_ANIMATION") != nullptr; + return enabled; +} + +std::mutex moduleMutex; +std::weak_ptr moduleInstance; +std::atomic workletFrames{0}; +std::atomic workletRafRequests{0}; + +class GPUIScheduler final : public worklets::UIScheduler { + protected: + bool queryIsOnUIThread() const override { + return gpui_worklets::isOnMainThread(); + } + + public: + void scheduleOnUI(std::function job) override { + static std::atomic scheduled{0}; + const auto count = ++scheduled; + if (animationTracingEnabled() && count <= 5) { + LOG(INFO) << "[gpui-worklets] scheduleOnUI count=" << count + << " onUI=" << isOnUIThread(); + } + if (isOnUIThread()) { + job(); + return; + } + UIScheduler::scheduleOnUI(std::move(job)); + rngpui::requestFrames("worklets", true); + } +}; + +worklets::RuntimeBindings::NativeLoggingHook nativeLoggingHook() { + return [](facebook::jsi::Runtime& runtime, + const facebook::jsi::Value&, + const facebook::jsi::Value* args, + size_t count) -> facebook::jsi::Value { + if (count != 2) { + throw std::invalid_argument("nativeLoggingHook takes 2 arguments"); + } + const auto message = args[0].asString(runtime).utf8(runtime); + LOG(INFO) << "[worklet] " << message; + return facebook::jsi::Value::undefined(); + }; +} +} // namespace + +namespace gpui_worklets { +namespace { +std::mutex uiThreadMutex; +std::thread::id uiThreadId; +} // namespace + +bool isOnMainThread() { + // The runtime records the main thread before its loop runs. `onFrame` only + // notes it once a frame arrives, and `UIScheduler::isOnUIThread` caches the + // first answer per thread: a query that comes first, such as a layout + // animation flush, would pin "not the UI thread" on the main thread for good. + if (rngpui::rn_gpui_is_main_thread()) { + return true; + } + std::lock_guard lock(uiThreadMutex); + return uiThreadId != std::thread::id{} && + uiThreadId == std::this_thread::get_id(); +} + +void noteMainThread() { + std::lock_guard lock(uiThreadMutex); + if (uiThreadId == std::thread::id{}) { + uiThreadId = std::this_thread::get_id(); + } +} + +Module::Module(std::shared_ptr jsInvoker) + : NativeWorkletsModuleSpec(std::move(jsInvoker)) {} + +Module::~Module() { + rngpui::reportPendingWork("worklets", 0); + rngpui::requestFrames("worklets", false); + if (initializer_) { + initializer_->invalidate(); + } + std::lock_guard lock(moduleMutex); + if (moduleInstance.lock().get() == this) { + moduleInstance.reset(); + } +} + +void Module::initialize(facebook::jsi::Runtime& runtime) { + if (initializer_) + return; + uiScheduler_ = std::make_shared(); + runtimeStatus_ = std::make_shared(); + jsScheduler_ = std::make_shared( + runtime, jsInvoker_, [] { return true; }); + std::weak_ptr weakModule = shared_from_this(); + auto runtimeBindings = + std::make_shared(worklets::RuntimeBindings{ + .requestAnimationFrame = + [weakModule](std::function callback) { + if (auto module = weakModule.lock()) { + module->requestAnimationFrame(std::move(callback)); + } + }, + .nativeLoggingHook = nativeLoggingHook(), + .networkingBackend = nullptr, + }); + initializer_ = std::make_shared( + jsScheduler_, uiScheduler_, runtimeBindings, runtimeStatus_); + LOG(INFO) << "[gpui-worklets] initialized runtime bindings"; + auto initializer = initializer_; + std::thread([initializer] { initializer->prepareProxy(); }).detach(); +} + +bool Module::installTurboModule( + facebook::jsi::Runtime& runtime, + bool bundleModeEnabled) { + initialize(runtime); + if (bundleModeEnabled) { + throw facebook::jsi::JSError( + runtime, "GPUI Worklets does not support Bundle Mode yet"); + } + proxy_ = initializer_->finalize(runtime, false, {}); + LOG(INFO) << "[gpui-worklets] WorkletsModule installed"; + return true; +} + +bool Module::prepareBundleMode(facebook::jsi::Runtime& runtime) { + throw facebook::jsi::JSError( + runtime, "GPUI Worklets does not support Bundle Mode yet"); +} + +bool Module::start(facebook::jsi::Runtime& runtime) { + if (!proxy_) + throw facebook::jsi::JSError( + runtime, "WorkletsModule.installTurboModule must run before start"); + proxy_->start(); + LOG(INFO) << "[gpui-worklets] UI runtime started"; + return true; +} + +bool Module::toggleSlowAnimationsOnUIRuntime(facebook::jsi::Runtime&) { + return false; +} + +void Module::requestAnimationFrame(std::function callback) { + const auto count = ++workletRafRequests; + if (animationTracingEnabled() && count <= 5) { + LOG(INFO) << "[gpui-worklets] requestAnimationFrame count=" << count; + } + { + std::lock_guard lock(callbacksMutex_); + frameCallbacks_.push_back(std::move(callback)); + } + rngpui::requestFrames("worklets", true); +} + +void Module::onFrame(double timestampMs) { + const auto count = ++workletFrames; + if (animationTracingEnabled()) { + static double windowStartMs = timestampMs; + static double previousFrameMs = timestampMs; + static double maximumGapMs = 0; + maximumGapMs = std::max(maximumGapMs, timestampMs - previousFrameMs); + previousFrameMs = timestampMs; + if (count % 60 == 0) { + LOG(INFO) << "[gpui-worklets] native frame cadence frames=60 elapsed_ms=" + << timestampMs - windowStartMs + << " max_frame_gap_ms=" << maximumGapMs; + windowStartMs = timestampMs; + maximumGapMs = 0; + } + } + if (animationTracingEnabled() && (count <= 5 || count % 600 == 0)) { + LOG(INFO) << "[gpui-worklets] GPUI frame count=" << count; + } + // Frame callbacks are platform entry points, outside Worklets' guarded + // invocation paths. Report JS exceptions on RN rather than unwinding across + // the CXX boundary and terminating the desktop process. + const auto reportError = [this](const facebook::jsi::JSError& error) { + LOG(ERROR) << "[gpui-worklets] " << error.what(); + worklets::JSLogger::reportFatalErrorOnJS( + jsScheduler_, + {.message = error.getMessage(), + .stack = error.getStack(), + .name = "WorkletsError"}); + }; + try { + if (uiScheduler_) + uiScheduler_->triggerUI(); + } catch (const facebook::jsi::JSError& error) { + reportError(error); + } + // Worklets has a perpetual native heartbeat even when its JS RAF queue is + // empty. Test synchronization observes application callbacks, not that beat. + if (std::getenv("RN_GPUI_TEST_DRIVER")) { + if (auto runtime = uiRuntime()) { + runtime->runSync([](facebook::jsi::Runtime& rt) { + auto global = rt.global(); + if (!global.hasProperty(rt, "requestAnimationFrame") || + (global.hasProperty(rt, "__gpuiTestRaf") && + facebook::jsi::Value::strictEquals( + rt, + global.getProperty(rt, "requestAnimationFrame"), + global.getProperty(rt, "__gpuiTestRaf")))) + return; + global.setProperty( + rt, + "__gpuiReportPendingRaf", + facebook::jsi::Function::createFromHostFunction( + rt, + facebook::jsi::PropNameID::forAscii( + rt, "__gpuiReportPendingRaf"), + 1, + [](facebook::jsi::Runtime&, + const facebook::jsi::Value&, + const facebook::jsi::Value* args, + size_t count) { + if (count == 1 && args[0].isNumber()) + rngpui::reportPendingWork( + "worklets", static_cast(args[0].getNumber())); + return facebook::jsi::Value::undefined(); + })); + rt.evaluateJavaScript( + std::make_shared(R"JS( + (() => { + const raf = globalThis.requestAnimationFrame; + const cancel = globalThis.cancelAnimationFrame; + const pending = new Set(); + const report = () => globalThis.__gpuiReportPendingRaf(pending.size); + globalThis.requestAnimationFrame = callback => { + let id; + id = raf(timestamp => { + pending.delete(id); report(); callback(timestamp); + }); + pending.add(id); report(); return id; + }; + globalThis.cancelAnimationFrame = id => { + pending.delete(id); report(); cancel(id); + }; + globalThis.__gpuiTestRaf = globalThis.requestAnimationFrame; + report(); + })(); + )JS"), + "gpui-test-raf.js"); + }); + } + } + std::vector> callbacks; + { + std::lock_guard lock(callbacksMutex_); + callbacks.swap(frameCallbacks_); + } + if (animationTracingEnabled() && !callbacks.empty() && + (count <= 5 || count % 60 == 0)) { + LOG(INFO) << "[gpui-worklets] callbacks on frame=" << callbacks.size(); + } + for (auto& callback : callbacks) { + try { + callback(timestampMs); + } catch (const facebook::jsi::JSError& error) { + reportError(error); + } + } + { + std::lock_guard lock(callbacksMutex_); + if (frameCallbacks_.empty()) { + rngpui::requestFrames("worklets", false); + } + } +} + +std::shared_ptr Module::uiRuntime() const { + return proxy_ ? proxy_->getUIWorkletRuntime() : nullptr; +} + +std::shared_ptr Module::uiScheduler() const { + return uiScheduler_; +} + +void onFrame(double timestampMs) { + noteMainThread(); + std::shared_ptr module; + { + std::lock_guard lock(moduleMutex); + module = moduleInstance.lock(); + } + if (module) + module->onFrame(timestampMs); +} + +std::shared_ptr currentModule() { + std::lock_guard lock(moduleMutex); + return moduleInstance.lock(); +} + +std::shared_ptr createModule( + const std::string& name, + std::shared_ptr jsInvoker) { + if (name != Module::kModuleName) + return nullptr; + auto module = std::make_shared(std::move(jsInvoker)); + { + std::lock_guard lock(moduleMutex); + moduleInstance = module; + } + return module; +} +} // namespace gpui_worklets + +std::shared_ptr create_gpui_worklets_module( + const std::string& name, + std::shared_ptr jsInvoker) { + return gpui_worklets::createModule(name, std::move(jsInvoker)); +} diff --git a/vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIWorkletsModule.h b/vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIWorkletsModule.h new file mode 100644 index 0000000000..49b817c41d --- /dev/null +++ b/vendor/react-native-gpui/modules/worklets/desktop/cpp/GPUIWorkletsModule.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace gpui_worklets { +bool isOnMainThread(); +void noteMainThread(); + +class Module final : public facebook::react::NativeWorkletsModuleSpec { + public: + explicit Module(std::shared_ptr jsInvoker); + ~Module() override; + + void onFrame(double timestampMs); + void requestAnimationFrame(std::function callback); + std::shared_ptr uiRuntime() const; + std::shared_ptr uiScheduler() const; + + bool installTurboModule( + facebook::jsi::Runtime& runtime, + bool bundleModeEnabled); + bool prepareBundleMode(facebook::jsi::Runtime& runtime); + bool start(facebook::jsi::Runtime& runtime); + bool toggleSlowAnimationsOnUIRuntime(facebook::jsi::Runtime& runtime); + + private: + void initialize(facebook::jsi::Runtime& runtime); + + std::shared_ptr uiScheduler_; + std::shared_ptr jsScheduler_; + std::shared_ptr runtimeStatus_; + std::shared_ptr initializer_; + std::shared_ptr proxy_; + std::mutex callbacksMutex_; + std::vector> frameCallbacks_; +}; + +void onFrame(double timestampMs); +std::shared_ptr currentModule(); +std::shared_ptr createModule( + const std::string& name, + std::shared_ptr jsInvoker); + +} // namespace gpui_worklets + +std::shared_ptr create_gpui_worklets_module( + const std::string& name, + std::shared_ptr jsInvoker); diff --git a/vendor/react-native-gpui/modules/worklets/desktop/cpp/GpuiWorkletsFrameModule.cpp b/vendor/react-native-gpui/modules/worklets/desktop/cpp/GpuiWorkletsFrameModule.cpp new file mode 100644 index 0000000000..a3ff29a3e5 --- /dev/null +++ b/vendor/react-native-gpui/modules/worklets/desktop/cpp/GpuiWorkletsFrameModule.cpp @@ -0,0 +1,13 @@ +#include "GpuiModules.h" + +namespace gpui_worklets { +void onFrame(double timestampMs); +} + +namespace rngpui { +void rn_gpui_install_worklets_module() { + registerFrameCallback( + [](double timestampMs) { gpui_worklets::onFrame(timestampMs); }); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/modules/worklets/package.json b/vendor/react-native-gpui/modules/worklets/package.json new file mode 100644 index 0000000000..aa73ec6828 --- /dev/null +++ b/vendor/react-native-gpui/modules/worklets/package.json @@ -0,0 +1,20 @@ +{ + "name": "@react-native-gpui/worklets", + "version": "0.1.0", + "description": "GPUI platform bindings for react-native-worklets", + "files": [ + "desktop", + "react-native.config.js", + "src" + ], + "peerDependencies": { + "react-native": "0.87.1", + "react-native-worklets": "0.13.x" + }, + "codegenConfig": { + "name": "GPUIWorkletsSpec", + "type": "modules", + "jsSrcsDir": "src" + }, + "license": "MIT" +} diff --git a/vendor/react-native-gpui/modules/worklets/react-native.config.js b/vendor/react-native-gpui/modules/worklets/react-native.config.js new file mode 100644 index 0000000000..6c33f0cfda --- /dev/null +++ b/vendor/react-native-gpui/modules/worklets/react-native.config.js @@ -0,0 +1,10 @@ +module.exports = { + dependency: { + platforms: { + desktop: { + cmake: 'desktop', + cppProvider: 'create_gpui_worklets_module', + }, + }, + }, +}; diff --git a/vendor/react-native-gpui/modules/worklets/src/NativeWorkletsModule.ts b/vendor/react-native-gpui/modules/worklets/src/NativeWorkletsModule.ts new file mode 100644 index 0000000000..a10c343352 --- /dev/null +++ b/vendor/react-native-gpui/modules/worklets/src/NativeWorkletsModule.ts @@ -0,0 +1,10 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { + installTurboModule(bundleModeEnabled: boolean): boolean; + prepareBundleMode(): boolean; + toggleSlowAnimationsOnUIRuntime(): boolean; + start(): boolean; +} + +export default TurboModuleRegistry.get('WorkletsModule'); diff --git a/vendor/react-native-gpui/packages/cli/assets/default-icon.png b/vendor/react-native-gpui/packages/cli/assets/default-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..576f7080edf3927b020760f2a8869318318e991b GIT binary patch literal 8867 zcmeHNYg|)TwqNHYkRVTiDhLRQfLf7IAD{*jt)iBKw05kmRE`(NN=1pH@({_Uf{%(Y z?X9Bx~>W z*#Euu+UKmjcJ_sZF18#sZWI7mE?u(lWdI?PLJ*tckJ9DYT>ul`TDoxlEAhFVjRA$u zwJE#Tyd1X4D`=N*St^sh?@dRmH_cw(b5N3>)e_Hf2PGK?cH8YRnYsRNH6GiPeV>1J zX-;?VT0zu*-|iaoS$R?D8>f|``32UlGM9lF0E*n+2z`Ik?PUk71A=`!7>J*gPd*UC zi&bYuzzMIz$}5Na0T*j311P;Xg;9Hg z%IsrTfK6VeDien6k?0uU=DiJ>ZlIV!8TY+R70)u{n8N^o#LAa!5JTy?n<5}R`6p8Blo}H&ZuAEvdH#2v{J`0-MOY0)U9Uj5jM!ZNc8s zh2r%#Fy>7ic4yhLHJ&i-EZT-H57Xm|VdY*bBmK}uWyNsJZX8E7YXX%khHo2Ojhm8XczMHlh93M zY5|im0XnL+h*-DJM&*+U9cOjeP`#Tm5jwsxk{@LJG@2ZsAR((~39rtDy1(?`w7BOF zoG~Mo4wUg=^EgdUF2oy$;ML>a1+Bai(UJqRre=baP&cT&GldP8&B#287V1xyb$LO< z*CHFVB%58g69Ecb=b}nv!X!;%D1@J*qx&D;xIKW;j0)sXr&aZRp*w`16`7-sRDJCR z&fQNZVQ~4r?ly@(WCx#1^w?@!Q&AQQ@hR3;=$54Dk}Na0tW7~ic2>v(DWh=;oQ&3b zPj)6Rh4?cfjDOB$c2lj8D0{v}#>&hS4`wjDh{gV>iTtuVNTV{hlSXmb{$LTHAAw{n zm$GXG@__>gbMoA@BJMeOzgdBduaXku8Q#{OnQTcJ9QS%?}|g!5xPwZk(cW$+ChpKlBW#^7lSHA8SdyO@);c1SwmP=8k&Li^gAo zG1lNd)((m6%~!0(LeS29q`9_@^TH|D$R1;h#L6|TR(24yV-3>WCnx4D1czHDIBW8Z zU$+hZqJ$LEUtU1jH#&F3(;lO?JvtmFM{Bx2GgP45k$&}3Xq+n=R%|y~VYiz>m!A6ei<}>a zY)A)fKvhh8h=9;d1(DI~Gw}!H{5pnjR3hc@X7D^CY$j(22X{QAldc>HrNb6lH*WBv zJ#L!(VYS%EAvNRhLg_T{*V7R)@0~p{tUhT7Kz;lVZ9_1-tcGB!$A3|Fh}tAz9G>kO z1sMi9fxxatO(^Ba`HMrN)zt>9Y8adq^&;Xp3L2dZDd*Ojkgh?r;V#fHfV+i6HvZ4W zr%V?Pn|3H{yg6u^P&2@c6+spCJvXg3dfVU><&;6lewMUS4@-^)`< zzQ^dfdXM_XCqc<6pCp2|pCYSnIbqp7h)hfwVOB%)3twG-=P3|I556KG?M5V_J{uFC zL^9+aqov8FP!7}sZTB10af1tQWQYgdBBuk9*FkGewK&V>O-cXC)S$)@d+0Y#L3OZT zRr7Br^W@+|8n)0sn&2?1qdqx*NW)Ez(lETtgK*=OecGNY-^*xA<^At5{+|Am)BP>R z5skqu_1n-G@fD55r*_MTOa8b^NOz{5Ax8~;Erv|}XwZZy`+JI-81B{XWv+qA*B(Te z**o@wcJ6$r`w$?y@}`K??`8NgLeS)=Cqw)+Ux>HItXy2-3Th7_Jh)Q;5H~~5a61Z% z__AEgv)lHI!MP#|G-L1?>F29wH2#GZyf<2S>-1LOd%`nQHY5n4INN6&NIWxzkfmUu zcrU))TDKfBojw5|=FTT9RJJEtV)_lMZ(I;Ub;UNP_ycsBz_6g57&jWYz`|xe7_Gi` zS_*t=cvVWf7N5<22^OJ4}<; z-~wb@$t6La@$otImOxGaF=AlG3*htvTln~4g>|?7ZcTIVBiH7EO?i2kyw$h?EVneo z7*-=_nBJ?u+tONKZdlFDcXt+*uG5t-6a%@?-&56`_^I1@GvI!1F?7m;@nu5VA9@h} zZmTj=IUQd#=&0>Q_m+%BNSZ7>YR$X;&`?%cf`YAjy{pE71yxSjV%i3o@l8gbj|WdX zbz{B+{R{C6=+k+netQ4+hGa|$QfL=-)nAGq1)jf*b{CJPJ-`24b?Y$b+hsH!$e|Mb z^I24qD&t3JeDk48gZ^)Nl$seBOhwlx`(a+L*?1NN70o_O zs;3u_VNWnL_*6n-C0YX?v z?R13G9%2ZK`)S4i3p34doV7TS40n{5VV-TA6Mm08JgF~e)Zz!189yT~pTHSzQ- zz!!%Xq+m|Wf16Z-`A%`5APbEi)4j()tKtpQ!U+Jc_Wl`@r}jjP@L)V&T*m^dbJaG7 z;xvGU>MU!h%e(EPIMyAbHwfeJc>IJ(;DK5J%wDS#JlMkGQ6=I9Ech3mCO#&=(6`$Lu{n}E{Z4KR!=P%^ReL=T~AR#GgSC<*jfxnzpv3oGKunZYtbY^T~ z)Ro_w!Xjoet@1+kjW4~Qs%}~X9(8fg&nf+~<*DMU5tulB2IvB)qC_36e!8Bzh~XoR zoZnPhza8cr+@SvINyPl0hB>=8_&v?JI>b>NU!^V?)KM%41-+2OTZ`MCPr0&q4#Znf z&i!qd#|&|>4joEY>WMlup(4P>XMqHLq+r1=0lMDS6v&d(bmlb zf8&`fGWzDlWw4i8Q1oxVZ(MI-hb`pNZ9yXaP7V&ES~cu~`tw653mJY2Rr6ox-kr?K z=`tPZD(%%7s*Mj~8^w}A74lgx6K9ac8uuA%H3TIrHt=Sbp~VXpQX5k*i82hArA85^ z`KmdEVXy9Pq27YMLNfXV+H<@#VUHNx%iNGwU9WNRg!KDokmg@-iu*hM0xV?t^=TBz ziaK?L33(}EGSb-zSF9WFBKR=Lp}+rSFFUom6sFF zY@GWBWZz4}8VzUJzSQs5`wZ7M))@gL9P(=V=!z-%F@oOfv~4+@v@6sFDGDvSj7LIp zx|gkw?E}?$C!9Q=eQAI6B@dQ=A%;4j)!*kIv#<MYX@(|(8+#2uWI&xO$GaA4jajz7F)(LNBV+r&wkgGdJe*p z>7~^9OD*2nmT=E+JO)FXb2GUG+Sdl?z7A8U2ZZ2?YeT+tG|0~7f$og)3Nt|6bXiEe zk|Zcpv!-=t20)J0U95%Y&~M3i2&;#4juOk8maWOLp6+R;$>lK@vnA+7I7OcwDj z{i{)j>~z!@#j+Z!K)NuGzWBP!9U9IVZ+~~FYX_vPRz9BYocL)KhRt}B?B@C+wwyM+ z!t%TJAqDEId!?*u0@f1&!mC2AcKS1_39jK(=q>wDysVBwlC+m3d}Zwg+ph~OF*crc zSqgRU(uL*RUA}b(QO^>j!)~BlG7G~! zG3>WPWfJT-u)z){QY0FW-O62H<4(O0;h^_s<=Hg+0)muTKqvu3R%E(%>(xPW{6 Fe*qyx8vg(Q literal 0 HcmV?d00001 diff --git a/vendor/react-native-gpui/packages/cli/bin/react-native-gpui.js b/vendor/react-native-gpui/packages/cli/bin/react-native-gpui.js new file mode 100755 index 0000000000..a506e8837e --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/bin/react-native-gpui.js @@ -0,0 +1,167 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { + collectNodeAddons, + createLibrary, + initDesktop, + initProject, + runDesktop, +} from '../src/commands.js'; +import { autolink } from '../src/autolink.js'; +const requestedCommand = process.argv[2]; +const command = requestedCommand === 'run-desktop' ? 'run' : requestedCommand; +const option = name => { + const index = process.argv.indexOf(name); + return index < 0 ? undefined : process.argv[index + 1]; +}; +const hasFlag = name => process.argv.includes(name); +const projectRoot = path.resolve(option('--project-root') ?? process.cwd()); +const desktopRoot = path.join(projectRoot, 'desktop'); +const run = (bin, args) => + execFileSync(bin, args, { cwd: projectRoot, stdio: 'inherit' }); + +if (hasFlag('--help') || hasFlag('-h')) { + console.log( + 'usage: npx react-native run-desktop [--project-root path] [--port 8082] [--release] [--features feature,...] [--no-launch]', + ); + process.exit(0); +} + +if (command === 'init-desktop') { + initDesktop({ projectRoot, skipInstall: hasFlag('--skip-install') }); + process.exit(0); +} + +if (command === 'create-library') { + createLibrary({ + libraryName: process.argv[3], + type: option('--type') ?? 'rust', + destination: option('--directory'), + moduleName: option('--module-name'), + packageName: option('--package-name'), + }); + process.exit(0); +} + +if (command === 'init') { + const appName = process.argv[3]; + if (!appName || !/^[A-Za-z][A-Za-z0-9_-]*$/u.test(appName)) { + console.error('usage: react-native-gpui init [--directory path]'); + process.exit(2); + } + const destination = path.resolve(option('--directory') ?? appName); + const initArgs = process.argv.slice(4); + for (let index = 0; index < initArgs.length; index += 1) { + if ( + initArgs[index] === '--directory' || + initArgs[index] === '--project-root' + ) { + initArgs.splice(index, 2); + index -= 1; + } + } + initProject({ appName, destination, initArgs }); + process.exit(0); +} + +if ( + !['autolink', 'bundle', 'collect-node-addons', 'prebuilt', 'run'].includes( + command, + ) +) { + console.error( + 'usage: react-native-gpui [options]', + ); + process.exit(2); +} + +if (command === 'collect-node-addons') { + collectNodeAddons({ + bundle: option('--bundle'), + out: option('--out'), + platform: option('--platform'), + }); + process.exit(0); +} + +if (command === 'run') { + await runDesktop({ + projectRoot, + entryFile: option('--entry-file'), + bundle: option('--bundle'), + config: option('--config'), + release: hasFlag('--release'), + features: option('--features'), + noLaunch: hasFlag('--no-launch'), + port: option('--port'), + host: option('--host'), + }); + process.exit(0); +} + +if (command === 'bundle') { + const entryFile = option('--entry-file') ?? 'index.js'; + const bundlePath = path.resolve( + option('--bundle') ?? path.join(desktopRoot, 'build/index.bundle'), + ); + fs.mkdirSync(path.dirname(bundlePath), { recursive: true }); + const bundleArgs = [ + 'react-native', + 'bundle', + '--platform', + 'desktop', + '--entry-file', + entryFile, + '--bundle-output', + bundlePath, + '--assets-dest', + path.join(path.dirname(bundlePath), 'assets'), + '--dev', + 'false', + ]; + const metroConfig = + option('--config') ?? path.join(desktopRoot, 'metro.config.js'); + if (fs.existsSync(metroConfig)) bundleArgs.push('--config', metroConfig); + process.exit(0); +} + +if (command === 'autolink') { + autolink({ projectRoot, configFile: option('--config-json') }); + process.exit(0); +} + +if (command === 'prebuilt') { + const appRequire = createRequire(path.join(projectRoot, 'package.json')); + let gpuiPackageJson; + try { + gpuiPackageJson = appRequire.resolve( + '@react-native-gpui/core/package.json', + ); + } catch { + console.error( + '@react-native-gpui/core is not installed in this project; run `npm install @react-native-gpui/core`.', + ); + process.exit(1); + } + const script = path.join( + path.dirname(gpuiPackageJson), + 'scripts', + 'prebuilt-binaries.mjs', + ); + const subcommand = process.argv[3]; + const allowed = ['fetch', 'path', 'verify', 'clean']; + if (!allowed.includes(subcommand)) { + console.error( + `usage: react-native-gpui prebuilt <${allowed.join('|')}> [--triple ] [dir]`, + ); + process.exit(1); + } + const forwarded = process.argv.slice(3); + const result = spawnSync(process.execPath, [script, ...forwarded], { + stdio: 'inherit', + }); + process.exit(result.status ?? 1); +} diff --git a/vendor/react-native-gpui/packages/cli/desktop.schema.json b/vendor/react-native-gpui/packages/cli/desktop.schema.json new file mode 100644 index 0000000000..b3896cdcf6 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/desktop.schema.json @@ -0,0 +1,249 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/streetwriters/react-native-gpui/packages/cli/desktop.schema.json", + "title": "React Native GPUI desktop app configuration", + "description": "The desktop/desktop.json file of a React Native GPUI app. Every field is optional: an app with no desktop.json runs and packages with the defaults.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "description": "Path to this schema, for editor completion.", + "type": "string" + }, + "productName": { + "description": "The name a person sees: window title bar, Dock, menu bar, installers. Defaults to the app's displayName, else its name.", + "type": "string", + "minLength": 1 + }, + "identifier": { + "description": "The reverse-DNS bundle id, e.g. com.example.myapp. Defaults to an id derived from the app name.", + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9.-]*$" + }, + "version": { + "description": "The app version. Defaults to the version in package.json.", + "type": "string", + "minLength": 1 + }, + "icon": { + "description": "One square PNG, recommended 1024x1024, relative to desktop/. Per-OS icon formats are generated from it. When this is unset the CLI uses its own placeholder, so every packaging format can be built.", + "type": "string", + "minLength": 1 + }, + "window": { + "description": "The main window.", + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "description": "The title bar text. Defaults to productName.", + "type": "string" + }, + "width": { + "description": "The window width in points. Defaults to 1024.", + "type": "number", + "exclusiveMinimum": 0 + }, + "height": { + "description": "The window height in points. Defaults to 768.", + "type": "number", + "exclusiveMinimum": 0 + }, + "position": { + "description": "Where the window opens. Omit to let the platform place it.", + "type": "object", + "additionalProperties": false, + "required": ["x", "y"], + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + } + }, + "resizable": { + "description": "Whether the window can be resized. Defaults to true.", + "type": "boolean" + }, + "restoreState": { + "description": "Whether the window remembers its size and position between launches. Defaults to true.", + "type": "boolean" + }, + "titleBar": { + "description": "How the window's title bar is drawn. 'default' keeps the platform's; 'transparent' keeps the window's chrome but lets the app draw under it, which on macOS leaves the traffic lights over the app's own bar; 'hidden' removes it entirely, and the app must draw and move its own bar. On Linux 'transparent' and 'hidden' both ask the compositor for client-side decorations, and a window manager that refuses leaves the native title bar in place. Defaults to 'default'.", + "default": "default", + "oneOf": [ + { + "type": "string", + "enum": ["default"] + }, + { + "type": "string", + "enum": ["transparent"] + }, + { + "type": "string", + "enum": ["hidden"] + } + ] + }, + "trafficLightPosition": { + "description": "Where the macOS traffic lights sit inside a transparent or hidden title bar, in points from the window's top-left corner. Ignored on Windows and Linux, which have no traffic lights. Defaults to the platform's own position.", + "type": "object", + "additionalProperties": false, + "required": ["x", "y"], + "properties": { + "x": { + "type": "number", + "minimum": 0 + }, + "y": { + "type": "number", + "minimum": 0 + } + } + }, + "menuBar": { + "description": "Whether the main window shows a menu bar. Windows and Linux draw one inside the window; on macOS this does nothing, because the bar belongs to the system. Defaults to true.", + "type": "boolean" + } + } + }, + "devMenu": { + "description": "Whether the development menu is available. Defaults to true in development builds.", + "type": "boolean" + }, + "tray": { + "description": "A menu-bar or notification-area icon for the app.", + "type": "object", + "additionalProperties": false, + "properties": { + "component": { + "description": "The React component to render as the tray popover.", + "type": "string", + "minLength": 1 + }, + "tooltip": { + "description": "The tooltip text.", + "type": "string" + }, + "icon": { + "description": "The tray icon, a PNG or JPEG path relative to desktop/. Without one the tray shows a built-in glyph.", + "type": "string", + "minLength": 1 + }, + "iconTemplate": { + "description": "macOS: draw the icon as a template the menu bar tints for light and dark mode. Use it for a monochrome glyph. Defaults to false.", + "type": "boolean" + }, + "accessory": { + "description": "Whether the app runs as an accessory, with no Dock icon. Defaults to true.", + "type": "boolean" + } + } + }, + "bundle": { + "description": "How the app is packaged.", + "type": "object", + "additionalProperties": false, + "properties": { + "targets": { + "description": "The formats to build. 'all' means every alpha format for the current OS.", + "oneOf": [ + { + "type": "string", + "enum": ["all"] + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "enum": ["app", "dmg", "nsis", "deb", "appimage"] + } + } + ] + }, + "resources": { + "description": "Extra files to ship, relative to desktop/.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "copyright": { + "description": "The copyright line shown by installers.", + "type": "string" + }, + "category": { + "description": "The app category, for the Linux desktop entry.", + "type": "string" + }, + "macos": { + "type": "object", + "additionalProperties": false, + "properties": { + "minimumSystemVersion": { + "description": "The oldest macOS the app supports. Defaults to 13.0.", + "type": "string" + }, + "entitlements": { + "description": "A path to an entitlements plist, relative to desktop/.", + "type": ["string", "null"] + }, + "infoPlist": { + "description": "Extra keys merged into the generated Info.plist.", + "type": "object" + } + } + }, + "windows": { + "type": "object", + "additionalProperties": false, + "properties": { + "nsis": { + "type": "object", + "additionalProperties": false, + "properties": { + "installMode": { + "description": "Whether the installer installs for the current user or for everyone.", + "type": "string", + "enum": ["currentUser", "perMachine", "both"] + } + } + } + } + }, + "linux": { + "type": "object", + "additionalProperties": false, + "properties": { + "deb": { + "type": "object", + "additionalProperties": false, + "properties": { + "depends": { + "description": "Extra Debian package dependencies.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "appimage": { + "type": "object", + "additionalProperties": false, + "properties": {} + } + } + } + } + } + } +} diff --git a/vendor/react-native-gpui/packages/cli/package.json b/vendor/react-native-gpui/packages/cli/package.json new file mode 100644 index 0000000000..252ef2ae9b --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/package.json @@ -0,0 +1,18 @@ +{ + "name": "@react-native-gpui/cli", + "version": "0.1.0", + "description": "React Native CLI integration and project scaffolding for the GPUI desktop platform.", + "files": [ + "bin", + "src", + "templates", + "assets", + "react-native.config.cjs", + "desktop.schema.json" + ], + "bin": { + "react-native-gpui": "./bin/react-native-gpui.js" + }, + "type": "module", + "dependencies": {} +} diff --git a/vendor/react-native-gpui/packages/cli/react-native.config.cjs b/vendor/react-native-gpui/packages/cli/react-native.config.cjs new file mode 100644 index 0000000000..94ea80747d --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/react-native.config.cjs @@ -0,0 +1,207 @@ +module.exports = { + commands: [ + { + name: 'run-desktop', + description: + 'Run the GPUI desktop target with Metro or as a release build', + func: async (_argv, _config, options) => { + const { runDesktop } = await import('./src/commands.js'); + await runDesktop({ + projectRoot: options.projectRoot, + entryFile: options.entryFile, + bundle: options.bundle, + config: options.config, + release: options.release, + port: options.port, + host: options.host, + features: options.features, + noLaunch: options.noLaunch === true || options.launch === false, + packager: options.packager !== false && options.noPackager !== true, + packagerLog: options.packagerLog === true, + terminal: options.terminal, + }); + }, + options: [ + { name: '--entry-file ', description: 'Application entry file' }, + { + name: '--release', + description: 'Bundle JavaScript and build the optimized app', + }, + { + name: '--port ', + description: 'Metro server port (default: 8081)', + }, + { + name: '--host ', + description: 'Metro server host (default: localhost)', + }, + { name: '--config ', description: 'Metro configuration file' }, + { name: '--bundle ', description: 'Bundle output path' }, + { + name: '--features ', + description: 'Comma-separated Cargo features for the desktop app', + }, + { + name: '--no-launch', + description: 'Build the desktop app without launching it', + }, + { + name: '--no-packager', + description: 'Do not start Metro if it is not already running', + }, + { + name: '--terminal ', + description: + 'Terminal app Metro opens in (default: $REACT_TERMINAL, then the platform default)', + }, + { + name: '--packager-log', + description: + 'Run Metro in the background, logging to desktop/build/metro.log', + }, + ], + examples: [ + { + desc: 'Run with Metro and Fast Refresh', + cmd: 'npx react-native run-desktop', + }, + { + desc: 'Build a bundled release', + cmd: 'npx react-native run-desktop --release', + }, + ], + }, + { + name: 'package-desktop', + description: + 'Build installable packages for this OS from desktop/desktop.json', + func: async (_argv, _config, options) => { + const { packageDesktop } = await import('./src/commands.js'); + await packageDesktop({ + projectRoot: options.projectRoot, + targets: options.target, + outDir: options.out, + }); + }, + options: [ + { + name: '--target ', + description: + 'Comma-separated formats: app, dmg, nsis, deb, appimage (default: all for this OS)', + }, + { + name: '--project-root ', + description: 'React Native project directory', + }, + { name: '--out ', description: 'Output directory' }, + ], + examples: [ + { + desc: 'Build every format this OS can produce', + cmd: 'npx react-native package-desktop', + }, + { + desc: 'Build only a dmg', + cmd: 'npx react-native package-desktop --target dmg', + }, + ], + }, + { + name: 'init-desktop', + description: 'Add GPUI desktop support to an existing React Native app', + func: async (_argv, _config, options) => { + const { initDesktop } = await import('./src/commands.js'); + initDesktop({ + projectRoot: options.projectRoot, + skipInstall: options.skipInstall, + }); + }, + options: [ + { + name: '--project-root ', + description: 'React Native project directory', + }, + { + name: '--skip-install', + description: 'Write the scaffold without installing packages', + }, + ], + examples: [ + { + desc: 'Add desktop support to this app', + cmd: 'npx react-native init-desktop', + }, + ], + }, + { + name: 'create-library ', + description: 'Create a GPUI TurboModule library in a new directory', + func: async (argv, _config, options) => { + const { createLibrary } = await import('./src/commands.js'); + createLibrary({ + libraryName: argv[0], + type: options.type, + destination: options.directory, + moduleName: options.moduleName, + packageName: options.packageName, + }); + }, + options: [ + { + name: '--type ', + description: 'Native implementation language (default: rust)', + }, + { name: '--directory ', description: 'Output directory' }, + { + name: '--module-name ', + description: 'Native TurboModule name', + }, + { name: '--package-name ', description: 'npm package name' }, + ], + examples: [ + { + desc: 'Create a Rust TurboModule package', + cmd: 'npx react-native create-library AwesomeStorage', + }, + { + desc: 'Create a C++ TurboModule package', + cmd: 'npx react-native create-library AwesomeStorage --type cpp', + }, + ], + }, + { + name: 'collect-node-addons', + description: + 'Copy the Node addons a bundle loads next to a packaged app executable', + func: async (_argv, _config, options) => { + const { collectNodeAddons } = await import('./src/commands.js'); + collectNodeAddons({ + bundle: options.bundle, + out: options.out, + platform: options.platform, + }); + }, + options: [ + { + name: '--bundle ', + description: 'The bundle file to scan for _requireNodeLibrary calls', + }, + { + name: '--out ', + description: 'Directory to copy the addons into', + }, + { + name: '--platform ', + description: + 'Target platform for shared-library matching (default: the host)', + }, + ], + examples: [ + { + desc: 'Collect the addons a release bundle loads', + cmd: 'npx react-native collect-node-addons --bundle build/index.bundle --out build/node-addons', + }, + ], + }, + ], +}; diff --git a/vendor/react-native-gpui/packages/cli/src/autolink.js b/vendor/react-native-gpui/packages/cli/src/autolink.js new file mode 100644 index 0000000000..d0b885cb5d --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/autolink.js @@ -0,0 +1,1462 @@ +import { Buffer } from 'node:buffer'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { createRequire } from 'node:module'; + +// npm ships npx as a .cmd shim on Windows, where child_process cannot resolve a +// command without its extension, and Node refuses to spawn .cmd/.bat files +// without a shell (the CVE-2024-27980 fix), so npx runs through the shell +// there. A shell joins the arguments into one command line, so arguments with +// spaces are quoted here first. +export const npxCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx'; +export const npxShell = process.platform === 'win32'; +export const npxArgs = args => + npxShell ? args.map(arg => (/\s/u.test(arg) ? `"${arg}"` : arg)) : args; + +/** + * Write a generated file only when its content differs. Cargo and CMake go by + * modification time, so rewriting an identical file makes every launch rebuild + * the C++ bridge and each crate that depends on it. + */ +function writeIfChanged(file, contents) { + try { + if (fs.readFileSync(file, 'utf8') === contents) return; + } catch {} + fs.writeFileSync(file, contents); +} + +const findCppSources = directory => { + if (!directory || !fs.existsSync(directory)) return []; + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) return findCppSources(entryPath); + return /\.(?:cc|cpp|cxx)$/u.test(entry.name) ? [entryPath] : []; + }); +}; + +/// Crates a module crate must depend on by version, never by path or git. +/// +/// They live under `@react-native-gpui/core` at a path that only exists inside +/// this repository. A published module crate resolves relative paths from +/// `node_modules/@react-native-gpui//desktop`, where `../../../packages` +/// is nowhere. So the crate names a crates.io version and autolink patches it, +/// in the app's root manifest, to the core that is actually installed. +const MANAGED_CRATES = [ + 'react-native-gpui', + 'rn-gpui-sys', + 'rn-gpui-module', + 'rn-gpui-module-build', + 'gpui', + 'gpui_platform', +]; + +/// The version each managed crate is patched to, when it is a path dependency. +const CORE_CRATE_VERSION = '0.1'; + +const PATCH_BEGIN = + '# Managed by @react-native-gpui/cli autolink; do not edit between these markers.'; +const PATCH_END = '# End managed by autolink.'; + +/** + * The gpui pin, read from core's manifest. + * + * Core owns the revision (`GPUI_PIN.md` says so), and a module that copied it + * would drift the moment the pin moved. The npm package ships `ReactGpui`, so + * the manifest is always beside the installed crate. + */ +function gpuiPin(platformRoot) { + const manifest = path.join(platformRoot, 'ReactGpui/Cargo.toml'); + let text; + try { + // Normalised for consistency with the lockfile read below. This pattern + // happens to survive CRLF on its own, because `\s*$` treats `\r` as + // whitespace, but relying on that is a trap for the next edit. + text = fs.readFileSync(manifest, 'utf8').replace(/\r\n/gu, '\n'); + } catch (error) { + throw new Error( + `could not read the gpui pin from ${manifest}: ${error.message}`, + ); + } + const workspace = /^\[workspace\.dependencies\]\s*$/mu.exec(text); + if (workspace === null) { + throw new Error(`${manifest} has no [workspace.dependencies] table`); + } + const start = workspace.index; + const nextTable = text.indexOf('\n[', start + 1); + const table = text.slice(start, nextTable < 0 ? text.length : nextTable); + const pin = name => { + const line = new RegExp(`^${name}\\s*=\\s*\\{([^}]*)\\}`, 'mu').exec(table); + if (line === null) { + throw new Error( + `${manifest} declares no ${name} in [workspace.dependencies]`, + ); + } + const git = /git\s*=\s*"([^"]+)"/u.exec(line[1]); + const rev = /rev\s*=\s*"([^"]+)"/u.exec(line[1]); + if (git === null || rev === null) { + throw new Error( + `${manifest}: ${name} is not a git dependency with a rev`, + ); + } + return { git: git[1], rev: rev[1] }; + }; + return { gpui: pin('gpui'), gpuiPlatform: pin('gpui_platform') }; +} + +/** + * The managed `[patch.crates-io]` block, or the text to append. + * + * `[patch]` only takes effect in the root manifest, which is why this is written + * into the app rather than into each module crate. + */ +function patchBlock(entries) { + return [ + PATCH_BEGIN, + ...entries.map(([name, spec]) => `${name} = ${spec}`), + PATCH_END, + ].join('\n'); +} + +/** + * A TOML string literal for a value. + * + * JSON's string syntax is a subset of TOML's for the characters paths contain, so + * quoting through it keeps a Windows path's backslashes escaped correctly. + */ +function escapeToml(value) { + return JSON.stringify(value); +} + +/** + * The `[patch.crates-io]` entries that point every managed crate at core. + * + * Exported because a second root manifest needs the same table: a module crate + * that declares `[workspace]` to become testable is its own root, and Cargo + * honours `[patch]` only there. Copying the block by hand reintroduces exactly + * the drift these entries exist to prevent — the gpui revision is read from + * core's manifest here, not written down anywhere. + * + * `platformRoot` is the installed core package, whose gated gpui pin is the + * authority; `base` is the directory the paths are written relative to, which + * for an app manifest is its `desktop/` directory. + */ +export function managedPatchEntries(platformRoot, base) { + const runtimeCrate = path.join(platformRoot, 'ReactGpui/rn-gpui'); + const pin = gpuiPin(platformRoot); + const relative = target => + escapeToml(path.relative(base, target).split(path.sep).join('/')); + return [ + ...MANAGED_CRATES.filter( + crate => crate !== 'gpui' && crate !== 'gpui_platform', + ).map(crate => [ + crate, + // The directory is not always the crate name: `react-native-gpui` lives + // in `rn-gpui`, so the path is built from the directory, not the key. + `{ path = ${relative( + crate === 'react-native-gpui' + ? runtimeCrate + : path.join(path.dirname(runtimeCrate), crate), + )} }`, + ]), + ['gpui', `{ git = "${pin.gpui.git}", rev = "${pin.gpui.rev}" }`], + [ + 'gpui_platform', + `{ git = "${pin.gpuiPlatform.git}", rev = "${pin.gpuiPlatform.rev}" }`, + ], + ]; +} + +/** + * Insert or replace the managed block in an app manifest. + * + * Idempotent: a second run must produce the same bytes, because `writeIfChanged` + * depends on it and a rewritten manifest makes Cargo rebuild everything. + */ +export function applyPatchBlock(manifest, manifestPath, entries) { + const block = patchBlock(entries); + const managed = new RegExp( + `${PATCH_BEGIN.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}[\\s\\S]*?${PATCH_END.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}`, + 'u', + ); + if (managed.test(manifest)) { + return manifest.replace(managed, block); + } + + const header = /^\[patch\.crates-io\]\s*$/mu.exec(manifest); + if (header !== null) { + // A table the user wrote. Cargo rejects a second header, so the block goes + // inside theirs — but a duplicate key would make the manifest invalid, and + // silently overwriting their line would be worse, so that is an error. + const start = header.index; + const nextTable = manifest.indexOf('\n[', start + 1); + const end = nextTable < 0 ? manifest.length : nextTable; + const body = manifest.slice(start, end); + for (const [name] of entries) { + if (new RegExp(`^${name}\\s*=`, 'mu').test(body)) { + throw new Error( + `${manifestPath}: [patch.crates-io] already sets ${name}, which autolink owns; remove that line so it can be managed`, + ); + } + } + const headerEnd = start + header[0].length; + return `${manifest.slice(0, headerEnd)}\n${block}${manifest.slice(headerEnd)}`; + } + + const separator = manifest.endsWith('\n') ? '' : '\n'; + return `${manifest}${separator}\n[patch.crates-io]\n${block}\n`; +} + +/// Every `[dependencies]`-style entry for a managed crate, with its raw value. +function managedDependencyEntries(manifest) { + const found = []; + let section = ''; + const lines = manifest.split('\n'); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const table = /^\[([^\]]+)\]\s*$/u.exec(line); + if (table !== null) { + section = table[1]; + // The table form puts one crate's keys on following lines: + // `[dependencies.gpui]` then `git = "…"`. Rare, but legal, and skipping it + // would leave a way past the check this function exists for. + const tableForm = /^dependencies\.([A-Za-z0-9_-]+)$/u.exec(section); + if (tableForm !== null && MANAGED_CRATES.includes(tableForm[1])) { + const keys = []; + for (let inner = index + 1; inner < lines.length; inner += 1) { + if (/^\[/u.test(lines[inner])) break; + keys.push(lines[inner]); + } + found.push({ crate: tableForm[1], value: keys.join('\n') }); + } + continue; + } + if (!/(?:^|[-.])dependencies$/u.test(section)) continue; + const entry = /^([A-Za-z0-9_-]+)\s*=\s*(.+?)\s*(?:#.*)?$/u.exec(line); + if (entry === null) continue; + const renamed = /package\s*=\s*"([^"]+)"/u.exec(entry[2]); + const crate = renamed === null ? entry[1] : renamed[1]; + if (!MANAGED_CRATES.includes(crate)) continue; + found.push({ crate, value: entry[2] }); + } + return found; +} + +/** + * Whether a version requirement accepts `version`, for the forms the ecosystem + * actually writes. Anything else is rejected rather than guessed at, because the + * failure mode is silent: a requirement the patch does not satisfy makes Cargo + * print one warning and resolve the real crates.io crate instead. + */ +function versionSatisfies(requirement, version) { + const text = requirement.trim(); + const parse = value => { + const parts = value.split('.').map(Number); + if (parts.some(Number.isNaN)) return undefined; + return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0]; + }; + const compare = (a, b) => { + for (let index = 0; index < 3; index += 1) { + if (a[index] !== b[index]) return a[index] - b[index]; + } + return 0; + }; + const exact = /^=\s*(\d+(?:\.\d+){0,2})$/u.exec(text); + if (exact !== null) return compare(parse(exact[1]), parse(version)) === 0; + const tilde = /^~\s*(\d+(?:\.\d+){0,2})$/u.exec(text); + if (tilde !== null) { + const [major, minor] = parse(tilde[1]); + const actual = parse(version); + return ( + actual[0] === major && + actual[1] === minor && + compare(actual, [major, minor, 0]) >= 0 + ); + } + const plain = /^\^?\s*(\d+(?:\.\d+){0,2})$/u.exec(text); + if (plain !== null) { + const parts = parse(plain[1]); + const actual = parse(version); + // Caret semantics: the leftmost non-zero component may not change. + if (parts[0] > 0) + return actual[0] === parts[0] && compare(actual, parts) >= 0; + if (parts[1] > 0) + return ( + actual[0] === 0 && actual[1] === parts[1] && compare(actual, parts) >= 0 + ); + return compare(actual, parts) === 0; + } + throw new Error(`unsupported version requirement "${text}"`); +} + +/** + * Reject a module crate that names a managed crate by path or git. + * + * `[patch]` cannot redirect a `path` dependency, and a git dependency of the + * same repository at another rev is rejected by Cargo. Either would fail for + * someone installing the package, a long way from the cause, so it is refused + * here with the fix in the message. A version requirement that the patch does + * not satisfy is worse than a failure: Cargo warns once and quietly resolves the + * real crates.io crate, so `gpui = "0.3"` would build against Zed's gpui. + */ +export function validateModuleManifest( + dependencyName, + cargoPath, + platformRoot, +) { + const manifestPath = path.join(cargoPath, 'Cargo.toml'); + let manifest; + try { + manifest = fs.readFileSync(manifestPath, 'utf8'); + } catch (error) { + throw new Error( + `${dependencyName}: could not read ${manifestPath}: ${error.message}`, + ); + } + const patchedVersions = patchedCrateVersions(platformRoot); + for (const { crate, value } of managedDependencyEntries(manifest)) { + const expected = + crate === 'gpui' || crate === 'gpui_platform' + ? `{ version = "${patchedVersions[crate]}", features = [...] }` + : `${crate} = "${CORE_CRATE_VERSION}"`; + if (/\bpath\s*=/u.test(value) || /\bgit\s*=/u.test(value)) { + throw new Error( + `${dependencyName}: ${crate} must be a version requirement (e.g. ${expected}), not a path or git dependency; autolink points it at the installed @react-native-gpui/core`, + ); + } + // A bare form is `crate = "0.1"`, where the value is the requirement with its + // quotes still on; an inline table is `crate = { version = "0.1" }`. + const bare = /^"([^"]*)"$/u.exec(value.trim())?.[1]; + const requirement = + /\bversion\s*=\s*"([^"]+)"/u.exec(value)?.[1] ?? bare ?? value; + const version = patchedVersions[crate]; + if (!versionSatisfies(requirement, version)) { + throw new Error( + `${dependencyName}: ${crate} requires "${requirement}", which does not match the ${version} that autolink patches to; Cargo would resolve the crates.io crate instead`, + ); + } + } +} + +/** + * The version each managed crate resolves to once patched. + * + * Core crates are `0.1.0`. The gpui crates come from the git pin, so their + * versions are read from the lockfile that describes that revision — the + * numbers cannot be assumed, and `gpui`'s crates.io crate is also `0.2.2`, which + * is exactly the collision the version check exists to catch. + */ +function patchedCrateVersions(platformRoot) { + const versions = {}; + for (const crate of [ + 'react-native-gpui', + 'rn-gpui-sys', + 'rn-gpui-module', + 'rn-gpui-module-build', + ]) { + versions[crate] = CORE_CRATE_VERSION; + } + const lockPath = path.join(platformRoot, 'ReactGpui/Cargo.lock'); + let lock; + try { + // Same reason as the manifest above: the pattern is line-anchored. + lock = fs.readFileSync(lockPath, 'utf8').replace(/\r\n/gu, '\n'); + } catch (error) { + throw new Error(`could not read ${lockPath}: ${error.message}`); + } + for (const crate of ['gpui', 'gpui_platform']) { + const block = new RegExp( + `\\[\\[package\\]\\]\\nname = "${crate}"\\nversion = "([^"]+)"\\nsource = "git\\+https://github\\.com/streetwriters/gpui-fast[^"]*"`, + 'u', + ).exec(lock); + if (block === null) { + throw new Error( + `${lockPath} has no ${crate} from the gpui-fast git source`, + ); + } + versions[crate] = block[1]; + } + return versions; +} + +const cppIdentifier = /^[A-Za-z_][A-Za-z0-9_]*$/u; +const includePath = /^[A-Za-z0-9_./+-]+$/u; +const jsEngineName = /^[a-z][a-z0-9-]*$/u; +const prebuiltModes = ['auto', 'always', 'never']; + +/** + * A dependency's `platforms.desktop` entry. + * + * The platform used to be called `gpui`. Nothing was ever published under that + * name, so there is no compatibility path: a dependency that still says + * `platforms.gpui` is told so, rather than being silently skipped. + */ +function desktopPlatformFor(dependencyName, dependency) { + const platforms = dependency.platforms ?? {}; + if (platforms.desktop) return platforms.desktop; + if (platforms.gpui) { + throw new Error( + `${dependencyName}: platforms.gpui was renamed to platforms.desktop`, + ); + } + return undefined; +} + +/** + * The app's `project.desktop` entry. + * + * The same rename applies to the app as to a dependency: `project.gpui` is + * rejected rather than read, because an option left in the old key would be + * ignored silently and the build would quietly use the default. + */ +function desktopProjectFor(config) { + const project = config.project ?? {}; + if (project.desktop === undefined && project.gpui !== undefined) { + throw new Error( + 'project.gpui was renamed to project.desktop in react-native.config.js', + ); + } + return project.desktop; +} + +/** + * Reads the app's `project.desktop.prebuilt` option: `auto` uses the pinned + * prebuilt C++ runtime when it matches and builds from source otherwise, + * `always` fails the build instead of falling back, `never` builds from source. + */ +function prebuiltModeFor(project) { + const prebuilt = project?.prebuilt ?? 'auto'; + if (!prebuiltModes.includes(prebuilt)) { + throw new Error( + `project.desktop.prebuilt must be one of ${prebuiltModes.map(mode => `'${mode}'`).join(', ')}`, + ); + } + return prebuilt; +} + +/** + * Validates a dependency's `platforms.desktop.jsEngine` entry. Installing a + * package that declares one is how an app opts into an engine; the entry names + * the C registration symbol core calls at startup and the optional JS tooling + * hooks the CLI uses. + */ +function jsEngineConfigFor(dependencyName, packageRoot, jsEngine) { + if ( + typeof jsEngine !== 'object' || + jsEngine === null || + Array.isArray(jsEngine) + ) { + throw new Error( + `${dependencyName}: platforms.desktop.jsEngine must be an object`, + ); + } + const { + name, + register, + bundle, + runtimeFiles, + nodeApiExports, + nodeApiLibrary, + } = jsEngine; + if (typeof name !== 'string' || !jsEngineName.test(name)) { + throw new Error( + `${dependencyName}: platforms.desktop.jsEngine.name must be a lowercase engine name (${jsEngineName})`, + ); + } + if (typeof register !== 'string' || !cppIdentifier.test(register)) { + throw new Error( + `${dependencyName}: platforms.desktop.jsEngine.register must be a C identifier`, + ); + } + const resolveHook = (key, value) => { + if (value === undefined) return undefined; + if (typeof value !== 'string' || value.length === 0) { + throw new Error( + `${dependencyName}: platforms.desktop.jsEngine.${key} must be a path`, + ); + } + const resolved = path.resolve(packageRoot, value); + if (!fs.existsSync(resolved)) { + throw new Error( + `${dependencyName}: platforms.desktop.jsEngine.${key} does not exist: ${resolved}`, + ); + } + return resolved; + }; + // The Node-API export list is what the app's executable re-exports on Windows, + // so the name has to be a plain library name: it becomes the DLL half of each + // forwarded export (`napi_x = hermesvm.napi_x`). + if ( + nodeApiExports !== undefined && + (typeof nodeApiLibrary !== 'string' || + !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(nodeApiLibrary)) + ) { + throw new Error( + `${dependencyName}: platforms.desktop.jsEngine.nodeApiLibrary must be a library name when nodeApiExports is set`, + ); + } + return { + dependencyName, + name, + register, + bundle: resolveHook('bundle', bundle), + runtimeFiles: resolveHook('runtimeFiles', runtimeFiles), + nodeApiExports: resolveHook('nodeApiExports', nodeApiExports), + nodeApiLibrary: nodeApiExports === undefined ? undefined : nodeApiLibrary, + }; +} + +function componentConfigFor(dependencyName, gpui) { + const descriptors = gpui.componentDescriptors ?? []; + const headers = gpui.componentHeaders ?? []; + const sources = gpui.componentSources ?? []; + const includeDirs = gpui.componentIncludeDirs ?? []; + const sourcePackage = gpui.componentSourcePackage; + if ( + !Array.isArray(descriptors) || + !descriptors.every( + value => typeof value === 'string' && cppIdentifier.test(value), + ) + ) { + throw new Error( + `${dependencyName}: platforms.desktop.componentDescriptors must be an array of C++ identifiers`, + ); + } + if ( + !Array.isArray(headers) || + !headers.every( + value => + typeof value === 'string' && + includePath.test(value) && + value.endsWith('.h'), + ) + ) { + throw new Error( + `${dependencyName}: platforms.desktop.componentHeaders must be an array of C++ header paths`, + ); + } + if (headers.some(value => value.split('/').includes('..'))) { + throw new Error( + `${dependencyName}: componentHeaders must not escape the include search path`, + ); + } + if (descriptors.length > 0 && headers.length === 0) { + throw new Error( + `${dependencyName}: componentHeaders is required when componentDescriptors are declared`, + ); + } + if ( + !Array.isArray(sources) || + !sources.every(value => typeof value === 'string') + ) { + throw new Error( + `${dependencyName}: platforms.desktop.componentSources must be an array of source paths`, + ); + } + if ( + !Array.isArray(includeDirs) || + !includeDirs.every(value => typeof value === 'string') + ) { + throw new Error( + `${dependencyName}: platforms.desktop.componentIncludeDirs must be an array of paths`, + ); + } + if ( + sourcePackage !== undefined && + (typeof sourcePackage !== 'string' || + !/^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/u.test(sourcePackage)) + ) { + throw new Error( + `${dependencyName}: platforms.desktop.componentSourcePackage must be a package name`, + ); + } + return { + descriptors, + headers, + sources, + includeDirs, + sourcePackage, + hostComponents: hostComponentsFor(dependencyName, gpui), + }; +} + +/** + * The host components a package registers, from + * `platforms.desktop.hostComponents`. + */ +function hostComponentsFor(dependencyName, gpui) { + const hostComponents = gpui.hostComponents ?? []; + if (!Array.isArray(hostComponents)) { + throw new Error( + `${dependencyName}: platforms.desktop.hostComponents must be an array`, + ); + } + return hostComponents.map(entry => { + if ( + entry === null || + typeof entry !== 'object' || + typeof entry.name !== 'string' || + !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(entry.name) + ) { + throw new Error( + `${dependencyName}: each platforms.desktop.hostComponents entry needs a "name" that is a component identifier`, + ); + } + if (entry.measured !== undefined && typeof entry.measured !== 'boolean') { + throw new Error( + `${dependencyName}: hostComponents "${entry.name}" has a non-boolean "measured"`, + ); + } + if ( + entry.nativeSurface !== undefined && + typeof entry.nativeSurface !== 'boolean' + ) { + throw new Error( + `${dependencyName}: hostComponents "${entry.name}" has a non-boolean "nativeSurface"`, + ); + } + if (entry.container !== undefined && typeof entry.container !== 'boolean') { + throw new Error( + `${dependencyName}: hostComponents "${entry.name}" has a non-boolean "container"`, + ); + } + if (entry.measured === true && entry.container === true) { + throw new Error( + `${dependencyName}: hostComponents "${entry.name}" cannot be both "measured" and a "container"; a measured component is a leaf, and a leaf cannot hold children`, + ); + } + return { + name: entry.name, + measured: entry.measured === true, + nativeSurface: entry.nativeSurface === true, + container: entry.container === true, + }; + }); +} + +function generateComponentCodegen({ + appRequire, + packageJson, + packageRoot, + dependencyName, + gpui, + generatedRoot, + hostComponents = [], +}) { + const componentCodegenPackage = gpui.componentCodegenPackage; + let codegenPackageRoot = packageRoot; + let codegenPackageJson = packageJson; + if (componentCodegenPackage !== undefined) { + if ( + typeof componentCodegenPackage !== 'string' || + !/^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/u.test(componentCodegenPackage) + ) { + throw new Error( + `${dependencyName}: platforms.desktop.componentCodegenPackage must be a package name`, + ); + } + const packageJsonPath = appRequire.resolve( + `${componentCodegenPackage}/package.json`, + ); + codegenPackageRoot = path.dirname(packageJsonPath); + codegenPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + } + const codegenConfig = codegenPackageJson.codegenConfig; + if (!codegenConfig || !['components', 'all'].includes(codegenConfig.type)) + return { sources: [], libraryName: undefined }; + const jsSrcsDir = path.resolve( + codegenPackageRoot, + codegenConfig.jsSrcsDir ?? '', + ); + if (!fs.existsSync(jsSrcsDir)) { + throw new Error( + `${dependencyName}: Codegen source directory does not exist: ${jsSrcsDir}`, + ); + } + const specs = []; + const visit = directory => { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) visit(entryPath); + else if (/\.(?:js|jsx|ts|tsx)$/u.test(entry.name)) specs.push(entryPath); + } + }; + visit(jsSrcsDir); + const schema = appRequire( + '@react-native/codegen/lib/cli/combine/combine-js-to-schema', + ).combineSchemasInFileList(specs, 'desktop', null, codegenConfig.name); + if ( + !Object.values(schema.modules ?? {}).some( + module => module.type === 'Component', + ) + ) + return { sources: [], libraryName: undefined }; + const libraryName = codegenConfig.name; + if ( + typeof libraryName !== 'string' || + !/^[A-Za-z][A-Za-z0-9_]*$/u.test(libraryName) + ) { + throw new Error( + `${dependencyName}: codegenConfig.name must be a C++ identifier for Fabric component codegen`, + ); + } + const codegenPackage = appRequire.resolve( + '@react-native/codegen/package.json', + ); + const codegenRoot = path.dirname(codegenPackage); + const rnCodegen = appRequire( + path.join(codegenRoot, 'lib/generators/RNCodegen.js'), + ); + const outputDirectory = path.join( + generatedRoot, + libraryName, + 'react/renderer/components', + libraryName, + ); + // Generate into a staging directory and copy only what changed, as the + // TurboModule path below does: Codegen rewrites every file, and a fresh + // mtime on these headers reruns rn-gpui-sys's build script on every launch. + const stagingDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), 'rn-gpui-codegen-'), + ); + try { + const generated = rnCodegen.generate( + { + libraryName, + schema, + outputDirectory: stagingDirectory, + packageName: packageJson.name, + assumeNonnull: false, + }, + { + generators: [ + 'descriptors', + 'events', + 'props', + 'states', + 'shadow-nodes', + ], + }, + ); + if (generated !== true) { + throw new Error( + `${dependencyName}: React Native Codegen failed for Fabric components`, + ); + } + for (const file of findGeneratedFiles(stagingDirectory)) { + const target = path.join( + outputDirectory, + path.relative(stagingDirectory, file), + ); + fs.mkdirSync(path.dirname(target), { recursive: true }); + writeIfChanged(target, fs.readFileSync(file, 'utf8')); + } + } finally { + fs.rmSync(stagingDirectory, { recursive: true, force: true }); + } + let hostComponentLibrary; + if (hostComponents.length > 0) { + const specPaths = specs.filter(file => + /export\s+default\s+\(?codegenNativeComponent [component.name, component]), + ); + const { generateHostComponentCpp } = appRequire( + '@react-native-gpui/codegen/generate-host-component-cpp', + ); + const generatedCpp = generateHostComponentCpp({ + specPaths, + libraryName, + components, + }); + writeIfChanged( + path.join(outputDirectory, 'GpuiHostComponents.generated.cpp'), + generatedCpp, + ); + hostComponentLibrary = libraryName; + } + const sources = findCppSources(outputDirectory); + return { sources, libraryName, hostComponentLibrary }; +} + +const findGeneratedFiles = directory => { + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) return findGeneratedFiles(entryPath); + return [entryPath]; + }); +}; + +/** + * Generates the C++ TurboModule specs for one package: our own spec class in + * `.h` and React Native's `JSI.h`, which carries the + * struct helpers for named object types. + * + * Codegen rewrites every file it is handed, and CMake compiles the headers' + * dependents whenever they change, so its output is staged in a temporary + * directory and only the files whose content differs are copied over. + */ +function generateModuleCodegen({ + appRequire, + platformRequire, + packageJson, + dependencyName, + specPaths, + generatedRoot, + generatedLibraries, +}) { + const libraryName = packageJson.codegenConfig?.name; + if (typeof libraryName !== 'string' || !cppIdentifier.test(libraryName)) { + throw new Error( + `${dependencyName}: codegenConfig.name is required for a C++ TurboModule`, + ); + } + // Two packages cannot share one codegen directory: the second would overwrite + // the first's header. + if (generatedLibraries.has(libraryName)) { + throw new Error( + `${dependencyName}: codegenConfig.name ${libraryName} is already used by ${generatedLibraries.get(libraryName)}`, + ); + } + generatedLibraries.set(libraryName, dependencyName); + const schema = appRequire( + '@react-native/codegen/lib/cli/combine/combine-js-to-schema', + ).combineSchemasInFileList(specPaths, 'desktop', null, libraryName); + for (const name of Object.keys(schema.modules ?? {})) { + if (schema.modules[name].type !== 'NativeModule') { + delete schema.modules[name]; + } + } + const codegenPackage = appRequire.resolve( + '@react-native/codegen/package.json', + ); + const codegenRoot = path.dirname(codegenPackage); + const rnCodegen = appRequire( + path.join(codegenRoot, 'lib/generators/RNCodegen.js'), + ); + const outputDirectory = path.join(generatedRoot, libraryName); + const stagingDirectory = fs.mkdtempSync( + path.join(os.tmpdir(), 'rn-gpui-codegen-'), + ); + try { + const generated = rnCodegen.generate( + { + libraryName, + schema, + outputDirectory: stagingDirectory, + packageName: packageJson.name, + assumeNonnull: false, + }, + { generators: ['modulesCxx'] }, + ); + if (generated !== true) { + throw new Error( + `${dependencyName}: React Native Codegen failed for the C++ TurboModule spec`, + ); + } + for (const file of findGeneratedFiles(stagingDirectory)) { + const target = path.join( + outputDirectory, + path.relative(stagingDirectory, file), + ); + fs.mkdirSync(path.dirname(target), { recursive: true }); + writeIfChanged(target, fs.readFileSync(file, 'utf8')); + } + } finally { + fs.rmSync(stagingDirectory, { recursive: true, force: true }); + } + const specPathByModule = new Map( + specPaths.map(specPath => [ + path.basename(specPath).replace(/\.[^.]+$/u, ''), + specPath, + ]), + ); + const { generateCxxSpec } = platformRequire( + '@react-native-gpui/codegen/generate-cxx-spec', + ); + writeIfChanged( + path.join(outputDirectory, `${libraryName}.h`), + generateCxxSpec({ + libraryName, + schema, + specPaths: Object.fromEntries(specPathByModule), + }), + ); + return outputDirectory; +} + +export function autolink({ projectRoot = process.cwd(), configFile } = {}) { + projectRoot = path.resolve(projectRoot); + const desktopRoot = path.join(projectRoot, 'desktop'); + const autolinkRoot = path.join(desktopRoot, 'autolink'); + const config = JSON.parse( + configFile + ? fs.readFileSync(path.resolve(configFile), 'utf8') + : execFileSync(npxCommand, npxArgs(['react-native', 'config']), { + cwd: projectRoot, + encoding: 'utf8', + shell: npxShell, + }), + ); + const prebuilt = prebuiltModeFor(desktopProjectFor(config)); + const appRequire = createRequire(path.join(projectRoot, 'package.json')); + let platformPackageJson; + try { + platformPackageJson = appRequire.resolve( + '@react-native-gpui/core/package.json', + ); + } catch (error) { + throw new Error( + `could not resolve @react-native-gpui/core from ${projectRoot}: ${error.message}`, + ); + } + const platformRoot = fs.realpathSync(path.dirname(platformPackageJson)); + // The spec generator ships with the platform package, so resolve it from + // there rather than from the app. + const platformRequire = createRequire(platformPackageJson); + const { TypeScriptParser } = appRequire( + '@react-native/codegen/lib/parsers/typescript/parser', + ); + const typescriptParser = new TypeScriptParser(); + const findSpecFiles = directory => + fs + .readdirSync(directory) + .filter(entry => + /^(?:Native.*|RN.*ModuleSpec)(?:\.d)?\.ts$/u.test(entry), + ); + const moduleNameForSpec = specPath => { + const schema = typescriptParser.parseFile(specPath); + const modules = Object.values(schema.modules ?? {}); + if (modules.length !== 1 || modules[0].type !== 'NativeModule') { + throw new Error(`${specPath} must define exactly one TurboModule`); + } + return modules[0].moduleName; + }; + const runtimeCrate = path.join(platformRoot, 'ReactGpui/rn-gpui'); + if (!fs.existsSync(path.join(runtimeCrate, 'Cargo.toml'))) { + throw new Error( + `@react-native-gpui/core has no Rust crate at ${runtimeCrate}`, + ); + } + // Core owns the gpui revision; a module that copied it would drift as soon as + // the pin moved, and the copies already had. + const pin = gpuiPin(platformRoot); + const rustCrates = []; + const turboModuleSpecs = []; + const cmakeProjects = []; + const cppTurboModules = []; + const appComponentDescriptors = []; + const appComponentHeaders = []; + const appComponentSources = []; + const appComponentIncludeDirs = []; + const componentNames = new Map(); + const hostComponentNames = new Map(); + const hostComponents = []; + const hostComponentLibraries = new Set(); + const jsEngines = []; + const codegenRoot = path.join(autolinkRoot, 'codegen'); + const generatedLibraries = new Map(); + for (const [dependencyName, dependency] of Object.entries( + config.dependencies ?? {}, + )) { + const root = fs.realpathSync( + path.resolve( + dependency.root ?? + path.join(projectRoot, 'node_modules', dependencyName), + ), + ); + const packageJson = JSON.parse( + fs.readFileSync(path.join(root, 'package.json'), 'utf8'), + ); + const gpui = desktopPlatformFor(dependencyName, dependency); + if (!gpui) continue; + if (gpui.jsEngine !== undefined) { + jsEngines.push(jsEngineConfigFor(dependencyName, root, gpui.jsEngine)); + } + const componentConfig = componentConfigFor(dependencyName, gpui); + const componentSourceRoot = componentConfig.sourcePackage + ? path.dirname( + appRequire.resolve(`${componentConfig.sourcePackage}/package.json`), + ) + : root; + for (const descriptor of componentConfig.descriptors) { + const previousDependency = componentNames.get(descriptor); + if (previousDependency) { + throw new Error( + `${dependencyName}: component descriptor ${descriptor} is already declared by ${previousDependency}`, + ); + } + componentNames.set(descriptor, dependencyName); + } + appComponentDescriptors.push(...componentConfig.descriptors); + appComponentHeaders.push(...componentConfig.headers); + for (const hostComponent of componentConfig.hostComponents) { + const previousDependency = hostComponentNames.get(hostComponent.name); + if (previousDependency) { + throw new Error( + `${dependencyName}: host component ${hostComponent.name} is already declared by ${previousDependency}`, + ); + } + hostComponentNames.set(hostComponent.name, dependencyName); + hostComponents.push({ + ...hostComponent, + index: hostComponents.length, + dependencyName, + }); + } + const generatedCodegen = generateComponentCodegen({ + appRequire, + packageJson, + packageRoot: root, + dependencyName, + gpui, + generatedRoot: codegenRoot, + hostComponents: hostComponents.filter( + component => component.dependencyName === dependencyName, + ), + }); + const declaresHostComponents = + Array.isArray(gpui.hostComponents) && gpui.hostComponents.length > 0; + if ( + generatedCodegen.sources.length > 0 && + componentConfig.descriptors.length === 0 && + !declaresHostComponents + ) { + throw new Error( + `${dependencyName}: GPUI component Codegen is enabled, but platforms.desktop.componentDescriptors is empty`, + ); + } + if (generatedCodegen.hostComponentLibrary) { + hostComponentLibraries.add(generatedCodegen.hostComponentLibrary); + } + appComponentSources.push( + ...generatedCodegen.sources, + ...componentConfig.sources.map(source => + path.resolve(componentSourceRoot, source), + ), + ); + if (generatedCodegen.sources.length > 0 && generatedCodegen.libraryName) { + appComponentIncludeDirs.push( + path.join(codegenRoot, generatedCodegen.libraryName), + ); + } + appComponentIncludeDirs.push( + ...componentConfig.includeDirs.map(directory => + path.resolve(componentSourceRoot, directory), + ), + ); + const specDirectory = + packageJson.codegenConfig?.jsSrcsDir ?? gpui.specs ?? 'src'; + if (gpui.cppProvider) { + if ( + typeof gpui.cppProvider !== 'string' || + !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(gpui.cppProvider) + ) { + throw new Error( + `${dependencyName}: platforms.desktop.cppProvider must be a C++ function name`, + ); + } + const specDir = path.join(root, specDirectory); + const cppSpecPaths = []; + for (const file of findSpecFiles(specDir)) { + const specPath = path.join(specDir, file); + const moduleName = moduleNameForSpec(specPath); + cppSpecPaths.push(specPath); + cppTurboModules.push({ + dependencyName, + moduleName, + provider: gpui.cppProvider, + hasCmake: Boolean(gpui.cmake), + sources: gpui.cppSources + ? gpui.cppSources.map(source => path.resolve(root, source)) + : gpui.cmake + ? findCppSources(path.resolve(root, gpui.cmake)) + : [], + }); + turboModuleSpecs.push(specPath); + } + if (cppSpecPaths.length > 0) { + const moduleCodegenDirectory = generateModuleCodegen({ + appRequire, + platformRequire, + packageJson, + dependencyName, + specPaths: cppSpecPaths, + generatedRoot: codegenRoot, + generatedLibraries, + }); + appComponentIncludeDirs.push(moduleCodegenDirectory); + for (const entry of cppTurboModules) { + if ( + entry.dependencyName === dependencyName && + entry.specs === undefined + ) { + entry.specs = cppSpecPaths; + entry.headers = findGeneratedFiles(moduleCodegenDirectory).filter( + file => file.endsWith('.h'), + ); + } + } + } + } + if (!gpui.crate) { + if (gpui.cmake) cmakeProjects.push(path.resolve(root, gpui.cmake)); + continue; + } + const crateName = gpui.crate; + if (typeof crateName !== 'string' || !/^[A-Za-z0-9_-]+$/u.test(crateName)) { + throw new Error( + `${dependencyName}: platforms.desktop.crate must be a Cargo crate name`, + ); + } + if ( + !gpui.path && + !fs.existsSync(path.join(root, 'desktop/Cargo.toml')) && + fs.existsSync(path.join(root, 'gpui/Cargo.toml')) + ) { + throw new Error( + `${dependencyName}: the gpui/ folder was renamed to desktop/`, + ); + } + const defaultCargoPath = fs.existsSync( + path.join(root, 'desktop/Cargo.toml'), + ) + ? 'desktop' + : '.'; + const cargoPath = path.resolve(root, gpui.path ?? defaultCargoPath); + if (!fs.existsSync(path.join(cargoPath, 'Cargo.toml'))) { + throw new Error(`${dependencyName}: no Cargo.toml at ${cargoPath}`); + } + const alias = crateName.replaceAll('-', '_'); + const moduleTypes = []; + const specDir = path.join(root, specDirectory); + if (fs.existsSync(specDir)) { + for (const file of findSpecFiles(specDir)) { + const specPath = path.join(specDir, file); + const moduleName = moduleNameForSpec(specPath); + moduleTypes.push(moduleName); + turboModuleSpecs.push(specPath); + } + } + validateModuleManifest(dependencyName, cargoPath, platformRoot); + rustCrates.push({ + dependencyName, + packageName: packageJson.name, + crateName, + alias, + cargoPath, + moduleTypes, + }); + if (gpui.cmake) cmakeProjects.push(path.resolve(root, gpui.cmake)); + } + + // Node addons are loaded from the stock `.node` files a package ships, at + // the absolute path the Babel plugin embeds (see + // @react-native-gpui/core/babel-plugin-node-addons.js). Nothing is staged here any + // more, but an app directory from an earlier run may still hold the old + // staging tree, so remove it. + fs.rmSync(path.join(autolinkRoot, 'node-api'), { + recursive: true, + force: true, + }); + + const localDesktopSource = path.join(projectRoot, 'desktop/src'); + if (fs.existsSync(localDesktopSource)) { + for (const file of findSpecFiles(localDesktopSource)) { + const specPath = path.join(localDesktopSource, file); + moduleNameForSpec(specPath); + turboModuleSpecs.push(specPath); + } + } + + const engineNames = new Map(); + for (const engine of jsEngines) { + const previousDependency = engineNames.get(engine.name); + if (previousDependency) { + throw new Error( + `${engine.dependencyName}: JS engine '${engine.name}' is already declared by ${previousDependency}`, + ); + } + engineNames.set(engine.name, engine.dependencyName); + } + if (jsEngines.length === 0) { + throw new Error( + 'no JS engine installed; add @react-native-gpui/hermes (or another engine package) to the app dependencies', + ); + } + + const generatedDir = autolinkRoot; + fs.mkdirSync(generatedDir, { recursive: true }); + const manifest = { + prebuilt, + rustCrates, + turboModuleSpecs, + cppTurboModules, + cmakeProjects, + appComponentDescriptors, + hostComponents, + appComponentHeaders: [...new Set(appComponentHeaders)], + appComponentSources: [...new Set(appComponentSources)], + appComponentIncludeDirs: [...new Set(appComponentIncludeDirs)], + jsEngines, + }; + writeIfChanged( + path.join(generatedDir, 'autolink.json'), + `${JSON.stringify(manifest, null, 2)}\n`, + ); + + const autolinkCrate = generatedDir; + fs.mkdirSync(path.join(autolinkCrate, 'src'), { recursive: true }); + const generatedDeps = rustCrates + .map(({ alias, crateName, cargoPath }) => { + const relativePath = path + .relative(autolinkCrate, cargoPath) + .split(path.sep) + .join('/'); + return `${alias} = { package = ${escapeToml(crateName)}, path = ${escapeToml(relativePath)} }`; + }) + .join('\n'); + writeIfChanged( + path.join(autolinkCrate, 'Cargo.toml'), + `[package]\nname = "rn-gpui-autolink"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nreact-native-gpui = { path = ${escapeToml( + path.relative(autolinkCrate, runtimeCrate).split(path.sep).join('/'), + )} }\n${generatedDeps}`, + ); + const ensureLinked = rustCrates.map( + ({ alias }) => ` ${alias}::rn_gpui_register();`, + ); + const generatedEnsureLinked = [ + 'pub fn ensure_linked() {', + ' unsafe {', + ' rn_gpui_initialize_app_turbo_modules();', + ' }', + ' declare_host_components();', + ...ensureLinked, + ' ensure_host_components();', + '}', + ].join('\n'); + const hostComponentDeclarations = hostComponents + .map( + ({ index, name, measured, nativeSurface, container }) => + ` react_native_gpui::host_component::declare_host_component(${index}, ${JSON.stringify(name)}, react_native_gpui::host_component::HostComponentOptions { measured: ${measured}, native_surface: ${nativeSurface}, container: ${container} });`, + ) + .join('\n'); + const hostComponentChecks = hostComponents + .map( + ({ index, name }) => + ` expect_host_component(${index}, ${JSON.stringify(name)});`, + ) + .join('\n'); + const generatedEnsureHostComponents = [ + 'fn declare_host_components() {', + hostComponentDeclarations || ' // No package declares a host component.', + '}', + '', + 'fn expect_host_component(index: u32, name: &str) {', + ' assert!(', + ' react_native_gpui::host_component::is_registered(index),', + ' "{name}: declared under platforms.desktop.hostComponents but no rn_gpui_register() registered it; call react_native_gpui::host_component::register_component from the crate that owns the component.",', + ' );', + '}', + '', + 'pub fn ensure_host_components() {', + hostComponentChecks || ' // No package declares a host component.', + '}', + ].join('\n'); + writeIfChanged( + path.join(autolinkCrate, 'src/lib.rs'), + `// Generated by @react-native-gpui/cli autolink; do not edit.\nextern "C" {\n fn rn_gpui_initialize_app_turbo_modules();\n}\n${generatedEnsureLinked}\n\n${generatedEnsureHostComponents}\n`, + ); + + const appCargoManifest = path.join(desktopRoot, 'Cargo.toml'); + if (!fs.existsSync(appCargoManifest)) { + throw new Error(`missing desktop Cargo manifest: ${appCargoManifest}`); + } + let appCargo = fs.readFileSync(appCargoManifest, 'utf8'); + const runtimeCrateRelativePath = path + .relative(desktopRoot, runtimeCrate) + .split(path.sep) + .join('/'); + const runtimeCrateTomlPath = escapeToml(runtimeCrateRelativePath); + const runtimeDependencyPattern = /^(react-native-gpui\s*=\s*)\{([^\n]*)\}$/mu; + if (runtimeDependencyPattern.test(appCargo)) { + appCargo = appCargo.replace( + runtimeDependencyPattern, + (_line, prefix, properties) => { + const withoutOldPath = properties + .replace(/(?:^|,)\s*path\s*=\s*"[^"]*"\s*/gu, '') + .trim() + .replace(/^,\s*|\s*,\s*$/gu, ''); + return `${prefix}{ path = ${runtimeCrateTomlPath}${withoutOldPath ? `, ${withoutOldPath}` : ''} }`; + }, + ); + } else { + appCargo = appCargo.replace( + /^\[dependencies\]\s*$/mu, + section => + `${section}\nreact-native-gpui = { path = ${runtimeCrateTomlPath} }`, + ); + } + if (!/^react-native-gpui\s*=/mu.test(appCargo)) { + throw new Error(`could not add react-native-gpui to ${appCargoManifest}`); + } + if (!/^rn-gpui-autolink\s*=/mu.test(appCargo)) { + appCargo = appCargo.replace( + /^\[dependencies\]\s*$/mu, + section => `${section}\nrn-gpui-autolink = { path = "autolink" }`, + ); + if (!/^rn-gpui-autolink\s*=/mu.test(appCargo)) { + appCargo += `\n[dependencies]\nrn-gpui-autolink = { path = "autolink" }\n`; + } + } + // The app's own Rust TurboModules: its build script generates the glue, and + // main.rs registers the modules it declares. + const moduleCrate = path + .relative( + desktopRoot, + path.join(path.dirname(runtimeCrate), 'rn-gpui-module'), + ) + .split(path.sep) + .join('/'); + const moduleBuildCrate = path + .relative( + desktopRoot, + path.join(path.dirname(runtimeCrate), 'rn-gpui-module-build'), + ) + .split(path.sep) + .join('/'); + const moduleDependency = `rn-gpui-module = { path = ${escapeToml(moduleCrate)} }`; + if (!/^rn-gpui-module\s*=/mu.test(appCargo)) { + appCargo = appCargo.replace( + /^\[dependencies\]\s*$/mu, + section => `${section}\n${moduleDependency}`, + ); + } + const moduleBuildDependency = `rn-gpui-module-build = { path = ${escapeToml(moduleBuildCrate)} }`; + if (!/^rn-gpui-module-build\s*=/mu.test(appCargo)) { + if (/^\[build-dependencies\]\s*$/mu.test(appCargo)) { + appCargo = appCargo.replace( + /^\[build-dependencies\]\s*$/mu, + section => `${section}\n${moduleBuildDependency}`, + ); + } else { + appCargo += `\n[build-dependencies]\n${moduleBuildDependency}\n`; + } + } + if (!/^\[features\]\s*$/mu.test(appCargo)) { + appCargo = appCargo.replace( + /^\[dependencies\]\s*$/mu, + '[features]\nvisual-capture = ["react-native-gpui/visual-capture"]\ntest-driver = ["react-native-gpui/test-driver"]\n\n[dependencies]', + ); + } else { + const tableStart = appCargo.indexOf('[features]'); + const suffixStart = appCargo.indexOf( + '\n[', + tableStart + '[features]'.length, + ); + const tableEnd = suffixStart < 0 ? appCargo.length : suffixStart; + let featureLines = appCargo.slice( + tableStart + '[features]'.length, + tableEnd, + ); + if (!/^visual-capture\s*=/mu.test(featureLines)) + featureLines += '\nvisual-capture = ["react-native-gpui/visual-capture"]'; + if (!/^test-driver\s*=/mu.test(featureLines)) + featureLines += '\ntest-driver = ["react-native-gpui/test-driver"]'; + appCargo = `${appCargo.slice(0, tableStart)}[features]${featureLines}${appCargo.slice(tableEnd)}`; + } + // `[patch]` is only honoured in the root manifest, so the redirection lives in + // the app's rather than in each module crate. + appCargo = applyPatchBlock( + appCargo, + appCargoManifest, + managedPatchEntries(platformRoot, desktopRoot), + ); + writeIfChanged(appCargoManifest, appCargo); + const appMain = path.join(desktopRoot, 'src/main.rs'); + if (fs.existsSync(appMain)) { + let source = fs.readFileSync(appMain, 'utf8'); + if (!source.includes('rn_gpui_autolink::ensure_linked()')) { + if (!/fn main\s*\(\s*\)\s*\{/u.test(source)) { + throw new Error( + `${appMain} must define fn main() for GPUI autolinking`, + ); + } + source = source.replace( + /(fn main\s*\(\s*\)\s*\{)/u, + '$1\n rn_gpui_autolink::ensure_linked();', + ); + writeIfChanged(appMain, source); + } + } + const cppProviderPath = path.join( + generatedDir, + 'CppTurboModules.generated.cpp', + ); + const cppProviderCases = cppTurboModules + .map( + ({ moduleName, provider }) => + ` if (name == ${JSON.stringify(moduleName)}) return ::${provider}(name, jsInvoker);`, + ) + .join('\n'); + const hostComponentRegistrationDeclarations = [...hostComponentLibraries] + .map( + library => + `extern "C" void rn_gpui_register_host_components_${library}();`, + ) + .join('\n'); + const hostComponentRegistrationCalls = [...hostComponentLibraries] + .map(library => ` rn_gpui_register_host_components_${library}();`) + .join('\n'); + const componentHeaders = [...new Set(appComponentHeaders)] + .map(header => `#include <${header}>`) + .join('\n'); + const componentRegistration = appComponentDescriptors + .map( + descriptor => + ` addComponent(facebook::react::concreteComponentDescriptorProvider());`, + ) + .join('\n'); + writeIfChanged( + cppProviderPath, + `// Generated by @react-native-gpui/cli autolink; do not edit.\n#include \n#include \n#include \n#include \n#include \n#include \n${componentHeaders}\n${hostComponentRegistrationDeclarations}\n${[...new Set(cppTurboModules.map(({ provider }) => `extern std::shared_ptr ${provider}(const std::string&, std::shared_ptr);`))].join('\n')}\nextern "C" void rn_gpui_set_app_cpp_turbo_module_provider(std::shared_ptr (*)(const std::string&, std::shared_ptr));\nextern "C" void rn_gpui_initialize_app_turbo_modules() {\n${hostComponentRegistrationCalls}\n rn_gpui_set_app_cpp_turbo_module_provider(+[](const std::string& name, std::shared_ptr jsInvoker) -> std::shared_ptr {\n${cppProviderCases}\n return nullptr;\n });\n}\nnamespace rngpui {\nvoid rn_gpui_register_app_components(facebook::react::ComponentDescriptorProviderRegistry& registry) {\n static const std::unordered_set builtins = {"View", "Paragraph", "Text", "RawText", "Image", "ScrollView", "TextInput", "ModalHostView", "NativeViewHost"};\n std::unordered_set registered;\n auto addComponent = [®istry, ®istered](const facebook::react::ComponentDescriptorProvider& provider) {\n const std::string name(provider.name);\n if (builtins.contains(name) || !registered.insert(name).second) {\n std::cerr << "[rn-gpui] duplicate app Fabric component descriptor ignored: " << name << std::endl;\n return;\n }\n registry.add(provider);\n };\n${componentRegistration}\n}\n} // namespace rngpui\n`, + ); + const cppSources = [ + ...new Set([ + ...cppTurboModules + .filter(({ hasCmake }) => !hasCmake) + .flatMap(({ sources }) => sources), + ...appComponentSources, + ]), + ]; + const cmakePath = value => value.replaceAll('\\', '/').replaceAll('"', '\\"'); + const cmake = [ + ...cmakeProjects.map( + (directory, index) => + `add_subdirectory("${cmakePath(directory)}" "${cmakePath(path.join('${CMAKE_BINARY_DIR}', `rn_gpui_autolink_${index}`))}")`, + ), + `set(RN_GPUI_APP_CPP_PROVIDER_SOURCE "${cmakePath(cppProviderPath)}")`, + `set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES ${cppSources.map(source => `"${cmakePath(source)}"`).join(' ')})`, + `set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS ${appComponentIncludeDirs.map(directory => `"${cmakePath(directory)}"`).join(' ')})`, + // Core calls each registration symbol from the generated + // rn_gpui_install_modules(), so the engine registry is populated before + // the host selects an engine. + ...jsEngines.map( + engine => + `set_property(GLOBAL APPEND PROPERTY RN_GPUI_JS_ENGINE_REGISTRATIONS "${engine.register}")`, + ), + ].join('\n'); + writeIfChanged( + path.join(generatedDir, 'autolink.cmake'), + `# Generated by @react-native-gpui/cli autolink.\n${cmake}\n`, + ); + console.log( + `Autolinked ${rustCrates.length} GPUI Rust crate(s), ${cppTurboModules.length} C++ TurboModule(s), ${cmakeProjects.length} CMake project(s), and ${jsEngines.length} JS engine(s) [${jsEngines.map(({ name }) => name).join(', ')}].`, + ); + return config; +} diff --git a/vendor/react-native-gpui/packages/cli/src/autolink.test.js b/vendor/react-native-gpui/packages/cli/src/autolink.test.js new file mode 100644 index 0000000000..e2e2c1120e --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/autolink.test.js @@ -0,0 +1,697 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterAll, expect, test } from 'vitest'; +import { autolink } from './autolink.js'; + +// The repository root by lookup, not by counting parents: this file sits at a +// different depth before and after a folder move. +const repoRoot = (() => { + let dir = path.dirname(fileURLToPath(import.meta.url)); + for (;;) { + const manifest = path.join(dir, 'package.json'); + if (fs.existsSync(manifest)) { + try { + if (JSON.parse(fs.readFileSync(manifest, 'utf8')).workspaces) + return dir; + } catch {} + } + const parent = path.dirname(dir); + if (parent === dir) throw new Error('no workspace root above the test'); + dir = parent; + } +})(); +const temporaryRoots = []; + +const spec = `import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { + greet(name: string): string; + loadLater(name: string): Promise; +} + +export default TurboModuleRegistry.getEnforcing('Fake'); +`; + +/** + * A project small enough for autolink to run end to end: a manifest to resolve + * from, a desktop Cargo manifest, one JS engine, and one C++ TurboModule + * package. It lives under the repo so Node resolves the workspace packages. + */ +function createProject({ + withCodegenConfig = true, + duplicateLibrary = false, + project, +} = {}) { + const projectRoot = fs.mkdtempSync(path.join(repoRoot, '.local/autolink-')); + temporaryRoots.push(projectRoot); + const write = (relative, contents) => { + const target = path.join(projectRoot, relative); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, contents); + }; + write( + 'package.json', + `${JSON.stringify({ name: 'autolink-test-app', version: '0.0.1' }, null, 2)}\n`, + ); + write( + 'desktop/Cargo.toml', + '[package]\nname = "autolink-test-app"\nversion = "0.0.1"\nedition = "2021"\n\n[dependencies]\n', + ); + write( + 'node_modules/fake-engine/package.json', + `${JSON.stringify({ name: 'fake-engine', version: '0.0.1' }, null, 2)}\n`, + ); + const moduleManifest = { + name: 'fake-module', + version: '0.0.1', + ...(withCodegenConfig + ? { + codegenConfig: { + name: 'FakeSpec', + type: 'modules', + jsSrcsDir: 'src', + }, + } + : {}), + }; + write( + 'node_modules/fake-module/package.json', + `${JSON.stringify(moduleManifest, null, 2)}\n`, + ); + write('node_modules/fake-module/src/NativeFake.ts', spec); + write( + 'node_modules/fake-module/desktop/CMakeLists.txt', + 'set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES "${CMAKE_CURRENT_LIST_DIR}/cpp/FakeModule.cpp")\n', + ); + write('node_modules/fake-module/cpp/FakeModule.cpp', '// fake module\n'); + write( + 'node_modules/fake-module/react-native.config.js', + "module.exports = {dependency: {platforms: {desktop: {cmake: 'desktop', cppProvider: 'create_fake_module'}}}};\n", + ); + if (duplicateLibrary) { + write( + 'node_modules/fake-module-two/package.json', + `${JSON.stringify( + { + name: 'fake-module-two', + version: '0.0.1', + codegenConfig: { + name: 'FakeSpec', + type: 'modules', + jsSrcsDir: 'src', + }, + }, + null, + 2, + )}\n`, + ); + write('node_modules/fake-module-two/src/NativeFakeTwo.ts', spec); + write( + 'node_modules/fake-module-two/react-native.config.js', + "module.exports = {dependency: {platforms: {desktop: {cppProvider: 'create_fake_two_module'}}}};\n", + ); + } + const configPath = path.join(projectRoot, 'rn-config.json'); + fs.writeFileSync( + configPath, + `${JSON.stringify( + { + ...(project ? { project: { desktop: project } } : {}), + dependencies: { + 'fake-engine': { + root: path.join(projectRoot, 'node_modules/fake-engine'), + platforms: { + desktop: { + jsEngine: { + name: 'hermes', + register: 'rn_gpui_register_js_engine_hermes', + }, + }, + }, + }, + 'fake-module': { + root: path.join(projectRoot, 'node_modules/fake-module'), + platforms: { + desktop: { cmake: 'desktop', cppProvider: 'create_fake_module' }, + }, + }, + ...(duplicateLibrary + ? { + 'fake-module-two': { + root: path.join(projectRoot, 'node_modules/fake-module-two'), + platforms: { + desktop: { cppProvider: 'create_fake_two_module' }, + }, + }, + } + : {}), + }, + }, + null, + 2, + )}\n`, + ); + return { projectRoot, configPath }; +} + +afterAll(() => { + for (const root of temporaryRoots) { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test('generates both spec headers for a C++ TurboModule package', () => { + const { projectRoot, configPath } = createProject(); + autolink({ projectRoot, configFile: configPath }); + const codegenDirectory = path.join( + projectRoot, + 'desktop/autolink/codegen/FakeSpec', + ); + const ours = fs.readFileSync( + path.join(codegenDirectory, 'FakeSpec.h'), + 'utf8', + ); + const reactNative = fs.readFileSync( + path.join(codegenDirectory, 'FakeSpecJSI.h'), + 'utf8', + ); + expect(ours).toContain('#include '); + expect(ours).toContain( + 'class NativeFakeSpec : public rngpui::TurboModuleBase', + ); + expect(ours).toContain('kModuleName = "Fake"'); + expect(ours).toContain('this->template valueMethod<&T::greet>("greet");'); + expect(ours).toContain( + 'this->template promiseMethod<&T::loadLater>("loadLater");', + ); + expect(reactNative).toContain('NativeFakeCxxSpec'); + expect(reactNative).toContain('"Fake"'); + const cmake = fs.readFileSync( + path.join(projectRoot, 'desktop/autolink/autolink.cmake'), + 'utf8', + ); + const includeDirs = cmake + .split('\n') + .find(line => line.includes('RN_GPUI_APP_CPP_INCLUDE_DIRS')); + // The generated CMake normalises every path to `/`, which is what CMake + // accepts on every platform (a Windows `\` is an escape in a CMake string). + expect(includeDirs).toContain(codegenDirectory.replaceAll('\\', '/')); +}); + +const manifestOf = projectRoot => + JSON.parse( + fs.readFileSync( + path.join(projectRoot, 'desktop/autolink/autolink.json'), + 'utf8', + ), + ); + +test('writes the prebuilt mode into the manifest, defaulting to auto', () => { + const defaults = createProject(); + autolink({ + projectRoot: defaults.projectRoot, + configFile: defaults.configPath, + }); + expect(manifestOf(defaults.projectRoot).prebuilt).toBe('auto'); + + const always = createProject({ project: { prebuilt: 'always' } }); + autolink({ projectRoot: always.projectRoot, configFile: always.configPath }); + expect(manifestOf(always.projectRoot).prebuilt).toBe('always'); +}); + +test('throws on an unknown prebuilt mode', () => { + const { projectRoot, configPath } = createProject({ + project: { prebuilt: true }, + }); + expect(() => autolink({ projectRoot, configFile: configPath })).toThrow( + "project.desktop.prebuilt must be one of 'auto', 'always', 'never'", + ); +}); + +test('reads project.desktop without complaint', () => { + const { projectRoot, configPath } = createProject({ + project: { prebuilt: 'never' }, + }); + autolink({ projectRoot, configFile: configPath }); + expect(manifestOf(projectRoot).prebuilt).toBe('never'); +}); + +test('throws when a C++ TurboModule package has no codegenConfig.name', () => { + const { projectRoot, configPath } = createProject({ + withCodegenConfig: false, + }); + expect(() => autolink({ projectRoot, configFile: configPath })).toThrow( + 'fake-module: codegenConfig.name is required for a C++ TurboModule', + ); +}); + +test('throws when two packages share one codegenConfig.name', () => { + const { projectRoot, configPath } = createProject({ duplicateLibrary: true }); + expect(() => autolink({ projectRoot, configFile: configPath })).toThrow( + 'fake-module-two: codegenConfig.name FakeSpec is already used by fake-module', + ); +}); + +test('leaves both headers untouched when nothing changed', () => { + const { projectRoot, configPath } = createProject(); + autolink({ projectRoot, configFile: configPath }); + const codegenDirectory = path.join( + projectRoot, + 'desktop/autolink/codegen/FakeSpec', + ); + const headers = ['FakeSpec.h', 'FakeSpecJSI.h'].map(name => + path.join(codegenDirectory, name), + ); + const before = headers.map(header => fs.statSync(header).mtimeMs); + autolink({ projectRoot, configFile: configPath }); + const after = headers.map(header => fs.statSync(header).mtimeMs); + expect(after).toEqual(before); +}); + +test('registers a crate that has no TurboModule specs', () => { + const { projectRoot, configPath } = createProject(); + const crateRoot = path.join(projectRoot, 'node_modules/fake-paint'); + fs.mkdirSync(path.join(crateRoot, 'desktop/src'), { recursive: true }); + fs.writeFileSync( + path.join(crateRoot, 'package.json'), + `${JSON.stringify({ name: 'fake-paint', version: '0.0.1' }, null, 2)}\n`, + ); + fs.writeFileSync( + path.join(crateRoot, 'desktop/Cargo.toml'), + '[package]\nname = "fake-paint"\nversion = "0.0.1"\nedition = "2021"\n\n[dependencies]\n', + ); + fs.writeFileSync( + path.join(crateRoot, 'react-native.config.js'), + "module.exports = {dependency: {platforms: {desktop: {crate: 'fake-paint'}}}};\n", + ); + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + config.dependencies['fake-paint'] = { + root: crateRoot, + platforms: { desktop: { crate: 'fake-paint' } }, + }; + fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`); + + autolink({ projectRoot, configFile: configPath }); + const generated = fs.readFileSync( + path.join(projectRoot, 'desktop/autolink/src/lib.rs'), + 'utf8', + ); + expect(generated).toContain('fake_paint::rn_gpui_register();'); +}); + +/** + * A project with one Rust module crate, so the manifest checks have something to + * read. The crate's manifest is the argument: the tests below vary only that. + */ +function projectWithRustCrate(manifest) { + // No codegen config: `fake-module` declares a `cppProvider`, and a C++ + // TurboModule without a `codegenConfig.name` is refused before the manifest + // checks this fixture is about would run. + const { projectRoot, configPath } = createProject({ + withCodegenConfig: false, + }); + const config0 = JSON.parse(fs.readFileSync(configPath, 'utf8')); + delete config0.dependencies['fake-module']; + fs.writeFileSync(configPath, `${JSON.stringify(config0, null, 2)}\n`); + const crateRoot = path.join(projectRoot, 'node_modules/fake-rust'); + fs.mkdirSync(path.join(crateRoot, 'desktop'), { recursive: true }); + fs.writeFileSync( + path.join(crateRoot, 'package.json'), + `${JSON.stringify({ name: 'fake-rust', version: '0.0.1' }, null, 2)}\n`, + ); + fs.writeFileSync(path.join(crateRoot, 'desktop/Cargo.toml'), manifest); + fs.writeFileSync( + path.join(crateRoot, 'react-native.config.js'), + "module.exports = {dependency: {platforms: {desktop: {crate: 'fake-rust'}}}};\n", + ); + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + config.dependencies['fake-rust'] = { + root: crateRoot, + platforms: { desktop: { crate: 'fake-rust' } }, + }; + fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`); + return { projectRoot, configPath, crateRoot }; +} + +const CARGO_HEADER = + '[package]\nname = "fake-rust"\nversion = "0.0.1"\nedition = "2021"\n\n[dependencies]\n'; + +test('the app manifest gets a managed patch block pointing at core', () => { + const { projectRoot, configPath } = projectWithRustCrate( + `${CARGO_HEADER}rn-gpui-module = "0.1"\n`, + ); + autolink({ projectRoot, configFile: configPath }); + const appCargo = fs.readFileSync( + path.join(projectRoot, 'desktop/Cargo.toml'), + 'utf8', + ); + expect(appCargo).toContain('[patch.crates-io]'); + expect(appCargo).toContain( + '# Managed by @react-native-gpui/cli autolink; do not edit between these markers.', + ); + expect(appCargo).toMatch( + /^rn-gpui-module = \{ path = "[^"]+rn-gpui-module" \}$/mu, + ); + expect(appCargo).toMatch( + /^react-native-gpui = \{ path = "[^"]+rn-gpui" \}$/mu, + ); + // The rev must be core's, read from core's own manifest, not a copy. + const coreManifest = fs.readFileSync( + path.join(repoRoot, 'packages/core/ReactGpui/Cargo.toml'), + 'utf8', + ); + const rev = /^gpui = \{[^}]*rev = "([^"]+)"/mu.exec(coreManifest)?.[1]; + expect(rev).toBeDefined(); + expect(appCargo).toContain( + `gpui = { git = "https://github.com/streetwriters/gpui-fast", rev = "${rev}" }`, + ); +}); + +test('a second autolink leaves the app manifest byte-identical', () => { + const { projectRoot, configPath } = projectWithRustCrate( + `${CARGO_HEADER}rn-gpui-module = "0.1"\n`, + ); + autolink({ projectRoot, configFile: configPath }); + const manifest = path.join(projectRoot, 'desktop/Cargo.toml'); + const once = fs.readFileSync(manifest, 'utf8'); + autolink({ projectRoot, configFile: configPath }); + expect(fs.readFileSync(manifest, 'utf8')).toBe(once); +}); + +test('an unrelated user patch table is kept, with one header', () => { + const { projectRoot, configPath } = projectWithRustCrate( + `${CARGO_HEADER}rn-gpui-module = "0.1"\n`, + ); + const manifest = path.join(projectRoot, 'desktop/Cargo.toml'); + const original = fs.readFileSync(manifest, 'utf8'); + fs.writeFileSync( + manifest, + `${original}\n[patch.crates-io]\nserde = { path = "vendor/serde" }\n`, + ); + autolink({ projectRoot, configFile: configPath }); + const patched = fs.readFileSync(manifest, 'utf8'); + expect(patched).toContain('serde = { path = "vendor/serde" }'); + expect(patched.match(/^\[patch\.crates-io\]$/gmu)).toHaveLength(1); + expect(patched).toContain('rn-gpui-module = { path ='); +}); + +test('a user patch table that claims a managed key is refused', () => { + const { projectRoot, configPath } = projectWithRustCrate( + `${CARGO_HEADER}rn-gpui-module = "0.1"\n`, + ); + const manifest = path.join(projectRoot, 'desktop/Cargo.toml'); + const original = fs.readFileSync(manifest, 'utf8'); + fs.writeFileSync( + manifest, + `${original}\n[patch.crates-io]\ngpui = { path = "my-gpui" }\n`, + ); + expect(() => autolink({ projectRoot, configFile: configPath })).toThrow( + /already sets gpui/u, + ); +}); + +test('a module crate may not name core by path', () => { + const { projectRoot, configPath } = projectWithRustCrate( + `${CARGO_HEADER}rn-gpui-module = { path = "../../core/ReactGpui/rn-gpui-module" }\n`, + ); + expect(() => autolink({ projectRoot, configFile: configPath })).toThrow( + /must be a version requirement/u, + ); +}); + +test('a module crate may not pin gpui by git', () => { + const { projectRoot, configPath } = projectWithRustCrate( + `${CARGO_HEADER}rn-gpui-module = "0.1"\ngpui = { git = "https://github.com/streetwriters/gpui-fast", rev = "c380367" }\n`, + ); + expect(() => autolink({ projectRoot, configFile: configPath })).toThrow( + /must be a version requirement/u, + ); +}); + +test('a gpui requirement the patch does not satisfy is refused', () => { + // 0.3 would resolve to the crates.io crate instead, and the only sign would be + // one warning Cargo prints and nobody reads. + const { projectRoot, configPath } = projectWithRustCrate( + `${CARGO_HEADER}rn-gpui-module = "0.1"\ngpui = "0.3"\n`, + ); + expect(() => autolink({ projectRoot, configFile: configPath })).toThrow( + /does not match the 0\.2\.2/u, + ); +}); + +test('a managed crate in table form is checked too', () => { + // `[dependencies.gpui]` with its keys on the following lines is legal TOML, so + // it must not be a way past the path check. + const { projectRoot, configPath } = projectWithRustCrate( + `${CARGO_HEADER}rn-gpui-module = "0.1"\n\n[dependencies.gpui]\ngit = "https://github.com/streetwriters/gpui-fast"\nrev = "c380367"\n`, + ); + expect(() => autolink({ projectRoot, configFile: configPath })).toThrow( + /must be a version requirement/u, + ); +}); + +test('a CRLF lockfile still yields the managed block', () => { + // A Windows checkout with core.autocrlf gives these files CRLF endings. The + // lockfile pattern anchors on `\n`, so without normalising the read it finds no + // gpui entry and autolink throws on every Windows run. The manifest pattern + // survives CRLF on its own, because `\s*$` treats `\r` as whitespace. + const { projectRoot, configPath } = projectWithRustCrate( + `${CARGO_HEADER}rn-gpui-module = "0.1"\n`, + ); + const coreLock = path.join(repoRoot, 'packages/core/ReactGpui/Cargo.lock'); + const original = fs.readFileSync(coreLock, 'utf8'); + fs.writeFileSync(coreLock, original.replace(/\n/gu, '\r\n')); + try { + autolink({ projectRoot, configFile: configPath }); + const rev = /^gpui = \{[^}]*rev = "([^"]+)"/mu.exec( + fs.readFileSync( + path.join(repoRoot, 'packages/core/ReactGpui/Cargo.toml'), + 'utf8', + ), + )?.[1]; + expect( + fs.readFileSync(path.join(projectRoot, 'desktop/Cargo.toml'), 'utf8'), + ).toContain(`rev = "${rev}"`); + } finally { + fs.writeFileSync(coreLock, original); + } +}); + +/** + * Adds a package that declares host components: a codegen spec for each, a Rust + * crate, and `hostComponents` entries in the platform config. + */ +function addHostComponentPackage( + { projectRoot, configPath }, + { + name = 'fake-host', + library = 'FakeHostSpec', + components, + interfaceOnly = true, + }, +) { + const root = path.join(projectRoot, 'node_modules', name); + const write = (relative, contents) => { + const target = path.join(root, relative); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, contents); + }; + write( + 'package.json', + `${JSON.stringify( + { + name, + version: '0.0.1', + codegenConfig: { name: library, type: 'components', jsSrcsDir: 'src' }, + }, + null, + 2, + )}\n`, + ); + write( + 'desktop/Cargo.toml', + `[package]\nname = "${name}"\nversion = "0.0.1"\nedition = "2021"\n\n[dependencies]\n`, + ); + for (const component of components) { + write( + `src/${component.name}NativeComponent.ts`, + `import { codegenNativeComponent, type ViewProps } from 'react-native'; + +export interface NativeProps extends ViewProps { + title?: string; + count?: Int32; +} + +export default codegenNativeComponent('${component.name}'${ + interfaceOnly ? ', { interfaceOnly: true }' : '' + }); +`, + ); + } + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + config.dependencies[name] = { + root, + platforms: { + desktop: { + crate: name, + hostComponents: components, + }, + }, + }; + fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`); + return root; +} + +const generatedLib = projectRoot => + fs.readFileSync( + path.join(projectRoot, 'desktop/autolink/src/lib.rs'), + 'utf8', + ); + +const generatedProvider = projectRoot => + fs.readFileSync( + path.join(projectRoot, 'desktop/autolink/CppTurboModules.generated.cpp'), + 'utf8', + ); + +test('declares each host component with its index and layout flags', () => { + const project = createProject(); + addHostComponentPackage(project, { + components: [ + { name: 'FakeChart' }, + { name: 'FakeLabel', measured: true }, + { name: 'FakeCard', container: true, nativeSurface: true }, + ], + }); + autolink({ + projectRoot: project.projectRoot, + configFile: project.configPath, + }); + const lib = generatedLib(project.projectRoot); + expect(lib).toContain( + 'declare_host_component(0, "FakeChart", react_native_gpui::host_component::HostComponentOptions { measured: false, native_surface: false, container: false });', + ); + expect(lib).toContain( + 'declare_host_component(1, "FakeLabel", react_native_gpui::host_component::HostComponentOptions { measured: true, native_surface: false, container: false });', + ); + expect(lib).toContain( + 'declare_host_component(2, "FakeCard", react_native_gpui::host_component::HostComponentOptions { measured: false, native_surface: true, container: true });', + ); + // Declared before any package registers, then checked for code afterwards. + expect(lib.indexOf('declare_host_components();')).toBeLessThan( + lib.indexOf('fake_host::rn_gpui_register();'), + ); + expect(lib.indexOf('fake_host::rn_gpui_register();')).toBeLessThan( + lib.indexOf('ensure_host_components();'), + ); + expect(lib).toContain('expect_host_component(2, "FakeCard");'); +}); + +test('generates the C++ that registers a host component library and calls it at startup', () => { + const project = createProject(); + addHostComponentPackage(project, { + components: [{ name: 'FakeChart' }, { name: 'FakeCard', container: true }], + }); + autolink({ + projectRoot: project.projectRoot, + configFile: project.configPath, + }); + const generated = path.join( + project.projectRoot, + 'desktop/autolink/codegen/FakeHostSpec/react/renderer/components/FakeHostSpec/GpuiHostComponents.generated.cpp', + ); + const cpp = fs.readFileSync(generated, 'utf8'); + expect(cpp).toContain('entry.index = 0;'); + expect(cpp).toContain('entry.index = 1;'); + expect(cpp).toContain('entry.container = true;'); + expect(cpp).toContain( + 'extern "C" void rn_gpui_register_host_components_FakeHostSpec() {', + ); + // The app calls it explicitly: a static library member nothing references is + // dropped by the linker, and its static initialiser with it. + const provider = generatedProvider(project.projectRoot); + expect(provider).toContain( + 'extern "C" void rn_gpui_register_host_components_FakeHostSpec();', + ); + expect(provider).toContain( + ' rn_gpui_register_host_components_FakeHostSpec();', + ); + // The generated file is compiled with React Native's own output for the spec. + const cmake = fs.readFileSync( + path.join(project.projectRoot, 'desktop/autolink/autolink.cmake'), + 'utf8', + ); + expect(cmake).toContain('GpuiHostComponents.generated.cpp'); + expect(cmake).toContain('Props.cpp'); +}); + +test('numbers host components across packages in one global order', () => { + const project = createProject(); + addHostComponentPackage(project, { + name: 'fake-host', + library: 'FakeHostSpec', + components: [{ name: 'FakeChart' }], + }); + addHostComponentPackage(project, { + name: 'fake-host-two', + library: 'FakeHostTwoSpec', + components: [{ name: 'FakeOther' }], + }); + autolink({ + projectRoot: project.projectRoot, + configFile: project.configPath, + }); + const lib = generatedLib(project.projectRoot); + expect(lib).toContain('declare_host_component(0, "FakeChart"'); + expect(lib).toContain('declare_host_component(1, "FakeOther"'); +}); + +test('refuses a host component that is both measured and a container', () => { + const project = createProject(); + addHostComponentPackage(project, { + components: [{ name: 'FakeBoth', measured: true, container: true }], + }); + expect(() => + autolink({ + projectRoot: project.projectRoot, + configFile: project.configPath, + }), + ).toThrow(/cannot be both "measured" and a "container"/u); +}); + +test('refuses a host component whose spec is not interfaceOnly', () => { + const project = createProject(); + addHostComponentPackage(project, { + interfaceOnly: false, + components: [{ name: 'FakeChart' }], + }); + expect(() => + autolink({ + projectRoot: project.projectRoot, + configFile: project.configPath, + }), + ).toThrow( + /must be created with codegenNativeComponent\(\.\.\., \{ interfaceOnly: true \}\)/u, + ); +}); + +test('refuses a duplicate host component name across packages', () => { + const project = createProject(); + addHostComponentPackage(project, { + name: 'fake-host', + library: 'FakeHostSpec', + components: [{ name: 'FakeChart' }], + }); + addHostComponentPackage(project, { + name: 'fake-host-two', + library: 'FakeHostTwoSpec', + components: [{ name: 'FakeChart' }], + }); + expect(() => + autolink({ + projectRoot: project.projectRoot, + configFile: project.configPath, + }), + ).toThrow(/host component FakeChart is already declared by fake-host/u); +}); diff --git a/vendor/react-native-gpui/packages/cli/src/commands.js b/vendor/react-native-gpui/packages/cli/src/commands.js new file mode 100644 index 0000000000..06ada51228 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/commands.js @@ -0,0 +1,1782 @@ +import { Buffer } from 'node:buffer'; +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync, spawn } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { autolink, npxArgs, npxCommand, npxShell } from './autolink.js'; +import { desktopOs, loadDesktopConfig, targetsFor } from './desktop-config.js'; +import { + CARGO_PACKAGER_VERSION, + runtimeLibraries, + targetProfileDir, + toPackagerConfig, +} from './package-desktop.js'; +import { + cargoBuildRecording, + cargoTargetDirectory, + pruneTarget, + targetGcEnabled, + targetGcMaxAgeDays, +} from './target-gc.js'; + +const cliDir = path.dirname(fileURLToPath(import.meta.url)); +const templatesDir = path.resolve(cliDir, '../templates'); + +/** + * The app links the system xkbcommon and xcb libraries, and a missing one only + * surfaces at the end of a long build as a linker error, so check up front. + * A non-system pkg-config first on PATH (Linuxbrew, Nix) cannot see the system + * packages. Pure so it can be unit tested: `runPkgConfig` + * returns true, false, or 'missing-pkg-config'; `systemPkgConfigPath` returns + * the system pkg-config search path, or null when it cannot be determined. + * + * The message stays distribution-neutral: it names the pkg-config module and + * gives one install command per common package manager, and the path hint uses + * whatever this machine's system pkg-config reports rather than a hard-coded + * x86_64 Debian path. + */ +export function linuxBuildDependencyProblem( + platform, + runPkgConfig, + systemPkgConfigPath = () => null, +) { + if (platform !== 'linux') return null; + // The libraries the app links on Linux (GPUI's keyboard handling over X11). + const modules = [ + { + pc: 'xkbcommon', + packages: { + 'Debian/Ubuntu': 'libxkbcommon-dev', + Fedora: 'libxkbcommon-devel', + Arch: 'libxkbcommon', + }, + }, + { + pc: 'xkbcommon-x11', + packages: { + 'Debian/Ubuntu': 'libxkbcommon-x11-dev', + Fedora: 'libxkbcommon-x11-devel', + Arch: 'libxkbcommon-x11', + }, + }, + { + pc: 'xcb', + packages: { + 'Debian/Ubuntu': 'libxcb1-dev', + Fedora: 'libxcb-devel', + Arch: 'libxcb', + }, + }, + ]; + const installExamples = packages => + Object.entries(packages) + .map(([distribution, name]) => { + const manager = + distribution === 'Fedora' + ? 'dnf' + : distribution === 'Arch' + ? 'pacman' + : 'apt'; + const command = + manager === 'pacman' + ? `sudo pacman -S ${name}` + : `sudo ${manager} install ${name}`; + return `${distribution}: ${command}`; + }) + .join(', '); + for (const { pc, packages } of modules) { + const result = runPkgConfig(pc); + if (result === 'missing-pkg-config') { + return ( + 'pkg-config is required to build the desktop app on Linux. ' + + "Install your distribution's pkg-config package " + + '(Debian/Ubuntu: `sudo apt install pkg-config`, ' + + 'Fedora: `sudo dnf install pkgconf-pkg-config`, ' + + 'Arch: `sudo pacman -S pkgconf`), then re-run.' + ); + } + if (!result) { + const systemPath = systemPkgConfigPath(); + const hint = systemPath + ? `set PKG_CONFIG_PATH=${systemPath} and re-run.` + : 'point PKG_CONFIG_PATH at your system pkgconfig directories and re-run.'; + return ( + `Linux build dependency ${pc} was not found. ` + + `Install your distribution's development package for it ` + + `(${installExamples(packages)}). ` + + `If pkg-config cannot see the system packages (Linuxbrew or Nix ` + + `pkg-config first on PATH does this), ${hint}` + ); + } + } + return null; +} + +/** + * The system pkg-config search path, for the preflight hint. Uses + * `/usr/bin/pkg-config` explicitly: a Linuxbrew or Nix `pkg-config` first on + * PATH is exactly the situation the hint is about, and it reports its own + * prefix, not the system one. Returns null when the binary is missing or the + * query fails. + */ +function readSystemPkgConfigPath() { + const pkgConfig = '/usr/bin/pkg-config'; + if (!fs.existsSync(pkgConfig)) return null; + try { + const output = execFileSync( + pkgConfig, + ['--variable', 'pc_path', 'pkg-config'], + { encoding: 'utf8' }, + ).trim(); + return output || null; + } catch { + return null; + } +} + +const run = (bin, args, options = {}) => + execFileSync(bin, args, { + cwd: options.cwd ?? process.cwd(), + stdio: 'inherit', + env: options.env ?? process.env, + shell: options.shell ?? false, + }); + +function loadEngineBundle(projectRoot) { + const appRequire = createRequire(path.join(projectRoot, 'package.json')); + let platformPackageJson; + try { + platformPackageJson = appRequire.resolve( + '@react-native-gpui/core/package.json', + ); + } catch { + return null; + } + const { activeJsEngineBundle } = appRequire( + path.join(path.dirname(platformPackageJson), 'js-engine.js'), + ); + return activeJsEngineBundle(projectRoot); +} + +function copyFontAssets(projectRoot, bundlePath, config) { + const destination = path.join( + path.dirname(bundlePath), + 'assets', + 'assets', + 'fonts', + ); + fs.rmSync(destination, { recursive: true, force: true }); + const sources = []; + const appFonts = path.join(projectRoot, 'desktop/assets/fonts'); + if (fs.existsSync(appFonts)) sources.push({ root: appFonts, prefix: 'app' }); + for (const [name, dependency] of Object.entries(config.dependencies ?? {})) { + const root = + dependency.root ?? path.join(projectRoot, 'node_modules', name); + for (const assets of dependency.assets ?? []) { + sources.push({ + root: path.resolve(root, assets), + prefix: name.replaceAll('/', '-'), + }); + } + } + let copied = 0; + const copyDirectory = (sourceRoot, directory, prefix) => { + if (!fs.existsSync(directory)) return; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const source = path.join(directory, entry.name); + if (entry.isDirectory()) { + copyDirectory(sourceRoot, source, prefix); + } else if (/\.(?:ttf|otf)$/iu.test(entry.name)) { + const relative = path.relative(sourceRoot, source); + const target = path.join(destination, prefix, relative); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(source, target); + copied += 1; + } + } + }; + for (const source of sources) + copyDirectory(source.root, source.root, source.prefix); + console.log(`Prepared ${copied} desktop font asset(s) in ${destination}`); +} + +function cargoConfigSetsTargetDir(projectRoot) { + let directory = path.resolve(projectRoot); + while (true) { + for (const fileName of ['config.toml', 'config']) { + const configPath = path.join(directory, '.cargo', fileName); + if (!fs.existsSync(configPath)) continue; + const lines = fs.readFileSync(configPath, 'utf8').split(/\r?\n/u); + let inBuildTable = false; + for (const line of lines) { + const table = line.match(/^\s*\[([^\]]+)\]/u); + if (table) { + inBuildTable = table[1].trim() === 'build'; + } else if (inBuildTable && /^\s*target-dir\s*=/u.test(line)) { + return true; + } + } + } + const parent = path.dirname(directory); + if (parent === directory) return false; + directory = parent; + } +} + +/** + * The project the Metro on `host:port` serves: its root, `null` for a Metro + * that does not say (older versions), or `undefined` when none answers. + */ +export async function metroProjectRoot(host, port) { + try { + const response = await fetch(`http://${host}:${port}/status`, { + signal: AbortSignal.timeout(1000), + }); + if ( + !response.ok || + !(await response.text()).includes('packager-status:running') + ) + return undefined; + return response.headers.get('x-react-native-project-root'); + } catch { + return undefined; + } +} + +const samePath = (a, b) => { + const real = target => { + try { + return fs.realpathSync(target); + } catch { + return path.resolve(target); + } + }; + return real(a) === real(b); +}; + +/** + * The terminal Metro opens in: `--terminal`, then `$REACT_TERMINAL` (the + * variable React Native's own `run-ios`/`run-android` read), then the + * platform's default. + */ +export function metroTerminal( + terminal, + { + platform = process.platform, + env = process.env, + exists = command => { + try { + execFileSync('which', [command], { stdio: 'ignore' }); + return true; + } catch { + return false; + } + }, + } = {}, +) { + const requested = terminal ?? env.REACT_TERMINAL; + if (requested) return requested; + if (platform === 'darwin') { + if (env.TERM_PROGRAM === 'iTerm.app') return 'iTerm'; + return 'Terminal'; + } + if (platform === 'win32') return 'cmd.exe'; + return [ + 'x-terminal-emulator', + 'gnome-terminal', + 'konsole', + 'xfce4-terminal', + 'kitty', + 'alacritty', + 'xterm', + ].find(exists); +} + +const shellQuote = value => `'${String(value).replaceAll("'", "'\\''")}'`; + +/** + * Opens Metro in a terminal window of its own, where its keys (`r`, `d`, `j`) + * work, the way `run-ios` and `run-android` do. Returns false when no window + * could be opened. + */ +function startMetroInTerminal({ projectRoot, desktopRoot, args, terminal }) { + const require = createRequire(path.join(projectRoot, 'package.json')); + let cli; + try { + // `cli.js` is not in react-native's `exports`; `package.json` is. + cli = path.join( + path.dirname(require.resolve('react-native/package.json')), + 'cli.js', + ); + } catch { + return false; + } + if (!fs.existsSync(cli)) return false; + const scriptDir = path.join(desktopRoot, 'build'); + fs.mkdirSync(scriptDir, { recursive: true }); + try { + if (process.platform === 'win32') { + const script = path.join(scriptDir, 'metro.bat'); + const quoted = [process.execPath, cli, ...args].map(arg => `"${arg}"`); + fs.writeFileSync( + script, + `@echo off\r\ntitle Metro\r\ncd /d "${projectRoot}"\r\n${quoted.join(' ')}\r\npause\r\n`, + ); + // A detached child on Windows gets a console window of its own. + spawn('cmd.exe', ['/c', script], { + detached: true, + stdio: 'ignore', + windowsHide: false, + }).unref(); + return true; + } + const script = path.join( + scriptDir, + process.platform === 'darwin' ? 'metro.command' : 'metro.sh', + ); + fs.writeFileSync( + script, + [ + '#!/bin/sh', + `cd ${shellQuote(projectRoot)} || exit 1`, + `exec ${[process.execPath, cli, ...args].map(shellQuote).join(' ')}`, + '', + ].join('\n'), + { mode: 0o755 }, + ); + fs.chmodSync(script, 0o755); + if (process.platform === 'darwin') { + execFileSync('open', ['-a', terminal, script], { stdio: 'ignore' }); + return true; + } + // gnome-terminal takes the command after `--`; the others after `-e`. + const terminalArgs = /gnome-terminal$/u.test(terminal) + ? ['--', script] + : ['-e', script]; + const child = spawn(terminal, terminalArgs, { + detached: true, + stdio: 'ignore', + }); + child.on('error', () => {}); + child.unref(); + return true; + } catch { + return false; + } +} + +/** The previous behaviour: Metro in the background, logging to a file. */ +function startMetroInBackground({ projectRoot, desktopRoot, args }) { + const logPath = path.join(desktopRoot, 'build/metro.log'); + fs.mkdirSync(path.dirname(logPath), { recursive: true }); + const logFd = fs.openSync(logPath, 'a'); + const server = spawn( + npxCommand, + npxArgs(['react-native', ...args, '--no-interactive']), + { + cwd: projectRoot, + detached: true, + stdio: ['ignore', logFd, logFd], + shell: npxShell, + }, + ); + fs.closeSync(logFd); + server.unref(); + const state = { error: undefined, server, logPath }; + server.once('error', error => { + state.error = error; + }); + return state; +} + +async function ensureMetro({ + projectRoot, + desktopRoot, + host, + port, + config, + packager = true, + packagerLog = false, + terminal, +}) { + const runningFor = await metroProjectRoot(host, port); + if (runningFor !== undefined) { + if (runningFor !== null && !samePath(runningFor, projectRoot)) { + throw new Error( + `Metro at http://${host}:${port} is serving another project (${runningFor}). ` + + `Stop it, or run on another port with --port.`, + ); + } + console.log(`Reusing Metro at http://${host}:${port}`); + return; + } + if (!packager) { + console.log( + `Metro is not running at http://${host}:${port}; start it with npx react-native start --port ${port}.`, + ); + return; + } + + const args = ['start', '--host', host, '--port', String(port)]; + if (config) args.push('--config', config); + const interactive = process.stdin.isTTY && !process.env.CI; + const window = + !packagerLog && interactive ? metroTerminal(terminal) : undefined; + let background; + if ( + window && + startMetroInTerminal({ projectRoot, desktopRoot, args, terminal: window }) + ) { + console.log(`Starting Metro in a new ${window} window`); + } else { + background = startMetroInBackground({ projectRoot, desktopRoot, args }); + } + + for (let attempt = 0; attempt < 120; attempt += 1) { + if ((await metroProjectRoot(host, port)) !== undefined) { + console.log( + background + ? `Started Metro at http://${host}:${port} (log: ${background.logPath})` + : `Started Metro at http://${host}:${port}`, + ); + return; + } + if (background?.error) throw background.error; + if (background && background.server.exitCode !== null) break; + await new Promise(resolve => setTimeout(resolve, 500)); + } + throw new Error( + background + ? `Metro did not start at http://${host}:${port}. Check ${background.logPath} or run npx react-native start --port ${port}.` + : `Metro did not start at http://${host}:${port}. Check the Metro window or run npx react-native start --port ${port}.`, + ); +} + +export async function runDesktop({ + projectRoot = process.cwd(), + entryFile, + bundle, + config, + release = false, + port = 8081, + host = 'localhost', + packager = true, + packagerLog = false, + terminal, + features, + noLaunch = false, + packaging = false, +}) { + projectRoot = path.resolve(projectRoot); + const dependencyProblem = linuxBuildDependencyProblem( + process.platform, + pc => { + try { + execFileSync('pkg-config', ['--exists', pc], { stdio: 'ignore' }); + return true; + } catch (error) { + return error?.code === 'ENOENT' ? 'missing-pkg-config' : false; + } + }, + readSystemPkgConfigPath, + ); + if (dependencyProblem) throw new Error(dependencyProblem); + const desktopRoot = path.join(projectRoot, 'desktop'); + entryFile ??= + ['index.js', 'index.ts', 'index.tsx'].find(file => + fs.existsSync(path.join(projectRoot, file)), + ) ?? 'index.js'; + const bundlePath = path.resolve( + projectRoot, + bundle ?? path.join(desktopRoot, 'build/index.bundle'), + ); + const metroConfig = path.resolve( + projectRoot, + config ?? path.join(desktopRoot, 'metro.config.js'), + ); + + // Autolink first: the manifest it writes tells the bundle path which engine is + // active, and therefore which Metro transform and shipping format to use. + const rnConfig = autolink({ projectRoot }); + const engineBundle = loadEngineBundle(projectRoot); + + // What the runtime loads: the plain Metro bundle, or the engine's compiled + // format (`.hbc` bytecode, for example). + let runtimeBundlePath = bundlePath; + + if (release) { + fs.mkdirSync(path.dirname(bundlePath), { recursive: true }); + const bundleArgs = [ + 'react-native', + 'bundle', + '--platform', + 'desktop', + '--entry-file', + entryFile, + '--bundle-output', + bundlePath, + '--assets-dest', + path.join(path.dirname(bundlePath), 'assets'), + '--dev', + 'false', + ]; + if (fs.existsSync(metroConfig)) bundleArgs.push('--config', metroConfig); + run(npxCommand, npxArgs(bundleArgs), { cwd: projectRoot, shell: npxShell }); + + if (engineBundle && typeof engineBundle.compile === 'function') { + const extension = + typeof engineBundle.bundleExtension === 'string' + ? engineBundle.bundleExtension + : '.js'; + const compiledPath = `${bundlePath.replace(/\.[^./\\]+$/u, '')}${extension}`; + const compiled = engineBundle.compile({ + input: bundlePath, + output: compiledPath, + dev: false, + }); + runtimeBundlePath = path.resolve( + projectRoot, + compiled?.output ?? compiledPath, + ); + } + } else { + const numericPort = Number(port); + if ( + !Number.isInteger(numericPort) || + numericPort < 1 || + numericPort > 65535 + ) { + throw new Error(`--port must be an integer between 1 and 65535`); + } + await ensureMetro({ + projectRoot, + desktopRoot, + host, + port: numericPort, + config: fs.existsSync(metroConfig) ? metroConfig : undefined, + packager, + packagerLog, + terminal, + }); + } + copyFontAssets(projectRoot, bundlePath, rnConfig); + + const appCargoManifest = path.join(desktopRoot, 'Cargo.toml'); + if (!fs.existsSync(appCargoManifest)) { + throw new Error(`missing desktop Cargo manifest: ${appCargoManifest}`); + } + const cargoArgs = [noLaunch ? 'build' : 'run']; + if (release) cargoArgs.push('--release'); + const cargoFeatures = (features ?? '') + .split(',') + .map(feature => feature.trim()) + .filter(Boolean); + if (cargoFeatures.some(feature => !/^[A-Za-z0-9_/-]+$/u.test(feature))) { + throw new Error( + '--features must be a comma-separated list of Cargo features', + ); + } + if (cargoFeatures.length > 0) + cargoArgs.push('--features', cargoFeatures.join(',')); + cargoArgs.push('--manifest-path', appCargoManifest); + const cargoEnv = { + ...process.env, + RN_GPUI_BUNDLE: runtimeBundlePath, + RN_GPUI_ASSETS_DIR: path.join(path.dirname(bundlePath), 'assets', 'assets'), + RN_GPUI_SOURCE_URL: entryFile.replace(/\.[^.]+$/u, ''), + RN_GPUI_DEV_HOST: host, + RN_GPUI_DEV_PORT: String(port), + RN_GPUI_AUTOLINK_MANIFEST: path.join(desktopRoot, 'autolink/autolink.json'), + RN_GPUI_AUTOLINK_CMAKE: path.join(desktopRoot, 'autolink/autolink.cmake'), + }; + // An unbundled run has no Info.plist to name its icon, so the app is told + // where it is and sets its Dock icon itself. + if (!packaging) cargoEnv.RN_GPUI_APP_ICON = appIconPath(projectRoot); + // `package-desktop` sets this: the binary is about to be installed elsewhere, + // so `build.rs` emits only relative rpaths. A plain release build keeps the + // absolute ones, because it is run from this tree. + if (packaging) cargoEnv.RN_GPUI_PACKAGING = '1'; + if ( + process.env.CARGO_TARGET_DIR === undefined && + !cargoConfigSetsTargetDir(projectRoot) + ) { + cargoEnv.CARGO_TARGET_DIR = path.join(desktopRoot, 'build/target'); + } + if (targetGcEnabled()) { + // Cargo never cleans a target directory, so every build first drops the + // units no build has used for a week, then builds while recording what it + // used. The build is separate from the launch so its JSON messages are not + // mixed with the app's own output; `cargo run` then finds it all fresh. + const targetDir = cargoTargetDirectory(appCargoManifest, { + cwd: projectRoot, + env: cargoEnv, + }); + if (targetDir) pruneTarget(targetDir, { maxAgeDays: targetGcMaxAgeDays() }); + await cargoBuildRecording(['build', ...cargoArgs.slice(1)], { + cwd: projectRoot, + env: cargoEnv, + targetDir, + }); + if (!noLaunch) { + run('cargo', cargoArgs, { cwd: projectRoot, env: cargoEnv }); + } + } else { + run('cargo', cargoArgs, { + cwd: projectRoot, + env: cargoEnv, + }); + } + + // The file the runtime was told to load: the engine's compiled output in a + // release build, the Metro bundle otherwise. `package-desktop` stages it. + return { runtimeBundlePath, bundlePath }; +} + +const appPackageName = (projectRoot, packageJson) => + `react-native-gpui-${(packageJson.name ?? path.basename(projectRoot)) + .toLowerCase() + .replaceAll(/[^a-z0-9-]/gu, '-')}`; + +function snapshotTree(root, track, rememberDirectory, existingDirectories) { + if (!fs.existsSync(root)) { + rememberDirectory(root); + return; + } + existingDirectories.add(root); + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + const target = path.join(root, entry.name); + if (entry.isDirectory()) { + snapshotTree(target, track, rememberDirectory, existingDirectories); + } else track(target); + } +} + +function removeNewFiles(root, snapshots, existingDirectories) { + if (!fs.existsSync(root)) return; + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + const target = path.join(root, entry.name); + if (entry.isDirectory()) + removeNewFiles(target, snapshots, existingDirectories); + else if (!snapshots.has(target)) { + fs.rmSync(target, { force: true, recursive: true }); + } + } + if (!existingDirectories.has(root)) { + try { + fs.rmdirSync(root); + } catch {} + } +} + +const desktopFiles = directory => + fs.readdirSync(directory, { withFileTypes: true }).flatMap(entry => { + const fullPath = path.join(directory, entry.name); + return entry.isDirectory() + ? desktopFiles(fullPath) + : [path.relative(path.join(templatesDir, 'app'), fullPath)]; + }); + +/// A number as a Rust float literal. +function asFloat(value) { + return Number.isInteger(value) ? `${value}.0` : String(value); +} + +/** + * The `main.rs` an app's `desktop.json` describes. + * + * The window, dev-menu and tray chain is unchanged from when these options + * lived in `app.json`; only where they are read from moved. + */ +export function desktopLaunchConfig(config, configPath) { + const settings = config.window ?? {}; + const title = + settings.title === undefined + ? 'env!("RN_GPUI_APP_NAME")' + : JSON.stringify(settings.title); + const width = settings.width; + const height = settings.height; + + // `window_size` takes `f32`: a JSON number reaches Rust as `1024`, which is + // an integer literal and does not coerce. + const chain = [ + ` .window_title(${title})`, + ` .window_size(${asFloat(width)}, ${asFloat(height)})`, + ]; + if (settings.position !== undefined) { + const { x, y } = settings.position; + if (x === undefined || y === undefined) { + throw new Error( + `desktop/desktop.json window.position needs x and y: ${configPath}`, + ); + } + chain.push(` .window_position(${asFloat(x)}, ${asFloat(y)})`); + } + if (settings.restoreState === false) { + chain.push(' .restore_window_state(false)'); + } + if (settings.menuBar === false) { + // Only meaningful where the bar is drawn inside the window; macOS ignores it, + // and the `menuBarInWindow` capability says so. + chain.push(' .menu_bar(false)'); + } + if (settings.titleBar !== undefined && settings.titleBar !== 'default') { + // The default is the platform's own bar, so only the two custom kinds are + // emitted. The enum is spelled out in full because the generated file has no + // `use` lines to import it with. + const variant = settings.titleBar === 'hidden' ? 'Hidden' : 'Transparent'; + chain.push(` .title_bar(react_native_gpui::WindowTitleBar::${variant})`); + } + if (settings.trafficLightPosition !== undefined) { + const { x, y } = settings.trafficLightPosition; + if (x === undefined || y === undefined) { + throw new Error( + `desktop/desktop.json window.trafficLightPosition needs x and y: ${configPath}`, + ); + } + chain.push(` .traffic_light_position(${asFloat(x)}, ${asFloat(y)})`); + } + chain.push(` .dev_menu(${config.devMenu})`); + if (config.tray !== undefined) { + const tray = config.tray; + let trayChain = 'TrayConfig::default()'; + if (tray.component !== undefined) { + trayChain += `.component(${JSON.stringify(tray.component)})`; + } + if (tray.tooltip !== undefined) { + trayChain += `.tooltip(${JSON.stringify(tray.tooltip)})`; + } + if (tray.icon !== undefined) { + // Embedded, so a packaged app needs no file beside it. + const iconPath = JSON.stringify(`/${tray.icon.replaceAll('\\', '/')}`); + trayChain += `.icon_bytes(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), ${iconPath})))`; + trayChain += `.icon_template(${tray.iconTemplate === true})`; + } + if (tray.accessory === false) { + trayChain += '.accessory(false)'; + } + chain.push(` .tray(${trayChain})`); + } + chain.push(` .configure_window(|options| { + // GPUI window options are public: titlebar, resizability, display, + // decorations, min size, and other native window behavior. + options.is_resizable = ${settings.resizable === false ? 'false' : 'true'}; + })`); + + return `//! Project-owned desktop launch configuration. +//! +//! Generated by \`init-desktop\` from \`desktop/desktop.json\`; edit that file and +//! re-run \`npx react-native init-desktop\`, or this file directly. See +//! \`desktop/README.md\` for the full set of options. +//! +//! Add \`mod my_turbo_module;\` here for local Rust TurboModules. Put matching +//! \`Native*.ts\` specs beside this file; \`run-desktop\` discovers and codegens +//! those specs automatically. + +fn main() { + // A debug build knows where its own bundle is; a release build does not, and + // must not: the path would name the machine that built it. A packaged release + // app resolves the bundle beside its executable instead. + #[cfg(debug_assertions)] + if std::env::var_os("RN_GPUI_BUNDLE").is_none() { + std::env::set_var("RN_GPUI_BUNDLE", env!("RN_GPUI_DEFAULT_BUNDLE")); + } + let config = react_native_gpui::DesktopAppConfig::from_env_with_defaults( + env!("RN_GPUI_APP_NAME"), + env!("RN_GPUI_APP_ID"), + ) +${chain.join('\n')}; + + react_native_gpui::run_with(config); +} +`; +} + +/// The file that pulls the published type augmentation into an app's build. +const ENV_TYPES_FILE = 'react-native-gpui-env.d.ts'; +const ENV_TYPES_CONTENT = `/// +// Added by react-native-gpui. Do not edit: it is regenerated. +`; + +export function scaffoldDesktop( + projectRoot, + packageJson, + { preserveExisting = false, beforeWrite = () => {} } = {}, +) { + const desktopTemplate = path.join(templatesDir, 'app/desktop'); + for (const relativePath of desktopFiles(desktopTemplate)) { + const targetRelativePath = + relativePath === 'desktop/gitignore' + ? 'desktop/.gitignore' + : relativePath; + const target = path.join(projectRoot, targetRelativePath); + if (preserveExisting && fs.existsSync(target)) continue; + beforeWrite(target); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(path.join(templatesDir, 'app', relativePath), target); + } + + const cargoPath = path.join(projectRoot, 'desktop/Cargo.toml'); + if (!preserveExisting || !fs.existsSync(cargoPath)) { + beforeWrite(cargoPath); + fs.writeFileSync( + cargoPath, + `[package]\nname = ${JSON.stringify(appPackageName(projectRoot, packageJson))}\nversion = "0.1.0"\nedition = "2021"\n\n# Set CARGO_PROFILE_DEV_DEBUG=full to opt into full debug information.\n[profile.dev]\ndebug = "line-tables-only"\nsplit-debuginfo = "packed"\n\n# Dependencies are optimized even in a dev build: GPUI rebuilds and paints the\n# element tree on every frame, and they are only compiled once.\n[profile.dev.package."*"]\ndebug = false\nopt-level = 3\n\n# The C++ bridge picks its CMake build type from this crate's opt-level, and a\n# Release build of React Native has no dev server, inspector or Fast Refresh.\n[profile.dev.package.rn-gpui-sys]\nopt-level = 0\n\n[profile.release]\ndebug = "line-tables-only"\n# Cargo strips inside the linker, before dsymutil, so package-desktop writes the\n# dSYM and then strips. Full LTO: 2 MB smaller for 1.4x the build time.\nstrip = "none"\nsplit-debuginfo = "packed"\nlto = "fat"\ncodegen-units = 1\n\n[build-dependencies]\nserde_json = "1"\n\n[dependencies]\n`, + ); + } + + // The type augmentation for the desktop View props is global, and a TypeScript + // augmentation only applies when the file declaring it is compiled. This + // reference is what puts it in the app's compilation, the way Next.js and Vite + // write theirs, so `desktopDragRegion` and `onContextMenu` type-check without an + // import of the desktop entry point. + const envPath = path.join(projectRoot, ENV_TYPES_FILE); + if (!preserveExisting || !fs.existsSync(envPath)) { + beforeWrite(envPath); + fs.writeFileSync(envPath, ENV_TYPES_CONTENT); + } + + const appConfigPath = path.join(projectRoot, 'app.json'); + if (!fs.existsSync(appConfigPath)) { + throw new Error(`missing React Native app config: ${appConfigPath}`); + } + const appConfig = JSON.parse(fs.readFileSync(appConfigPath, 'utf8')); + if (typeof appConfig.name !== 'string' || appConfig.name.length === 0) { + throw new Error(`app.json must define a nonempty name: ${appConfigPath}`); + } + // Every launch setting comes from `desktop/desktop.json`; `app.json`'s old + // `desktop` key is rejected by the loader with a pointer to the new file. + const launchConfig = desktopLaunchConfig( + loadDesktopConfig(projectRoot, process.platform), + path.join(projectRoot, 'desktop/desktop.json'), + ); + if (launchConfig !== null) { + const mainPath = path.join(projectRoot, 'desktop/src/main.rs'); + if (!preserveExisting || !fs.existsSync(mainPath)) { + beforeWrite(mainPath); + fs.writeFileSync(mainPath, launchConfig); + } + } + + const updatedPackageJson = { + ...packageJson, + dependencies: { + ...packageJson.dependencies, + '@react-native-gpui/core': + packageJson.dependencies?.['@react-native-gpui/core'] ?? '^0.1.0', + '@react-native-gpui/hermes': + packageJson.dependencies?.['@react-native-gpui/hermes'] ?? '^0.1.0', + // `rn-gpui-module-build` resolves its spec generator from the app + // (`require.resolve('@react-native-gpui/codegen/generate-rust-spec')` with + // the crate directory as the search path), so the app must depend on it + // directly. Inside this repository the workspace hoists it and a missing + // entry goes unnoticed; an app installed from the registry fails with + // "Cannot find module '@react-native-gpui/codegen/generate-rust-spec'". + '@react-native-gpui/codegen': + packageJson.dependencies?.['@react-native-gpui/codegen'] ?? '^0.1.0', + }, + scripts: { + ...packageJson.scripts, + 'test:e2e': + packageJson.scripts?.['test:e2e'] ?? + 'vitest run --config desktop/vitest.e2e.config.ts', + }, + devDependencies: { + ...packageJson.devDependencies, + vitest: packageJson.devDependencies?.vitest ?? '^3.2.4', + '@react-native-gpui/testing': + packageJson.devDependencies?.['@react-native-gpui/testing'] ?? '^0.1.0', + '@react-native-gpui/cli': + packageJson.devDependencies?.['@react-native-gpui/cli'] ?? '^0.1.0', + }, + }; + return updatedPackageJson; +} + +export function initDesktop({ + projectRoot = process.cwd(), + skipInstall = false, +}) { + projectRoot = path.resolve(projectRoot); + const snapshots = new Map(); + const createdDirectories = new Set(); + const existingDirectories = new Set(); + const track = file => { + if (!snapshots.has(file)) { + snapshots.set(file, fs.existsSync(file) ? fs.readFileSync(file) : null); + } + }; + try { + const packagePath = path.join(projectRoot, 'package.json'); + if (!fs.existsSync(packagePath)) { + throw new Error(`no package.json found in ${projectRoot}`); + } + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + if ( + !packageJson.dependencies?.['react-native'] && + !packageJson.devDependencies?.['react-native'] + ) { + throw new Error('package.json does not declare react-native'); + } + + const updatedPackageJson = scaffoldDesktop(projectRoot, packageJson, { + preserveExisting: true, + beforeWrite(file) { + track(file); + let directory = path.dirname(file); + const missingDirectories = []; + while (directory.startsWith(projectRoot) && !fs.existsSync(directory)) { + missingDirectories.push(directory); + directory = path.dirname(directory); + } + missingDirectories.forEach(item => createdDirectories.add(item)); + }, + }); + track(packagePath); + fs.writeFileSync( + packagePath, + `${JSON.stringify(updatedPackageJson, null, 2)}\n`, + ); + + if (!skipInstall) { + const manager = fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml')) + ? 'pnpm' + : fs.existsSync(path.join(projectRoot, 'yarn.lock')) + ? 'yarn' + : fs.existsSync(path.join(projectRoot, 'bun.lockb')) || + fs.existsSync(path.join(projectRoot, 'bun.lock')) + ? 'bun' + : 'npm'; + for (const lock of [ + 'package-lock.json', + 'npm-shrinkwrap.json', + 'yarn.lock', + 'pnpm-lock.yaml', + 'bun.lock', + 'bun.lockb', + ]) { + const file = path.join(projectRoot, lock); + track(file); + } + run(manager, ['install'], { cwd: projectRoot }); + } + + const desktopRoot = path.join(projectRoot, 'desktop'); + const autolinkRoot = path.join(desktopRoot, 'autolink'); + const buildRoot = path.join(desktopRoot, 'build'); + const rememberDirectory = directory => { + for ( + let current = directory; + current.startsWith(projectRoot) && !fs.existsSync(current); + current = path.dirname(current) + ) { + createdDirectories.add(current); + } + }; + snapshotTree(autolinkRoot, track, rememberDirectory, existingDirectories); + const assetsRoot = path.join(buildRoot, 'assets'); + snapshotTree(assetsRoot, track, rememberDirectory, existingDirectories); + const bundlePath = path.join(buildRoot, 'index.bundle'); + track(bundlePath); + track(path.join(desktopRoot, 'Cargo.toml')); + track(path.join(desktopRoot, 'src/main.rs')); + autolink({ projectRoot }); + + if (!fs.existsSync(bundlePath)) { + rememberDirectory(assetsRoot); + run( + npxCommand, + npxArgs([ + 'react-native', + 'bundle', + '--platform', + 'desktop', + '--entry-file', + ['index.js', 'index.ts', 'index.tsx'].find(file => + fs.existsSync(path.join(projectRoot, file)), + ) ?? 'index.js', + '--bundle-output', + bundlePath, + '--assets-dest', + assetsRoot, + '--config', + path.join(desktopRoot, 'metro.config.js'), + '--dev', + 'false', + ]), + { cwd: projectRoot, shell: npxShell }, + ); + } + console.log( + `Added GPUI desktop support to ${projectRoot}. Run npx react-native run-desktop.`, + ); + } catch (error) { + removeNewFiles( + path.join(projectRoot, 'desktop/autolink'), + snapshots, + existingDirectories, + ); + removeNewFiles( + path.join(projectRoot, 'desktop/build/assets'), + snapshots, + existingDirectories, + ); + const bundlePath = path.join(projectRoot, 'desktop/build/index.bundle'); + if (snapshots.get(bundlePath) === null) { + fs.rmSync(bundlePath, { force: true }); + } + for (const [file, contents] of [...snapshots.entries()].reverse()) { + if (contents === null) fs.rmSync(file, { force: true }); + else fs.writeFileSync(file, contents); + } + for (const directory of [...createdDirectories].sort( + (left, right) => right.length - left.length, + )) { + try { + fs.rmdirSync(directory); + } catch {} + } + console.error( + `init-desktop failed and restored its file changes: ${error.message}`, + ); + console.error('Manual steps to add desktop support:'); + for (const step of [ + 'Install @react-native-gpui/core and @react-native-gpui/cli in the app.', + 'Copy the desktop/ scaffold from @react-native-gpui/cli/templates/app/desktop.', + 'Add react-native-gpui-env.d.ts to the app root: /// ', + 'Run npm install (or your package manager), then npx react-native run-desktop.', + ]) { + console.error(` - ${step}`); + } + throw error; + } +} + +export function initProject({ appName, destination, initArgs = [] }) { + destination = path.resolve(destination); + const destinationExisted = fs.existsSync(destination); + if (destinationExisted && fs.readdirSync(destination).length) { + throw new Error(`destination is not empty: ${destination}`); + } + fs.mkdirSync(path.dirname(destination), { recursive: true }); + try { + run( + npxCommand, + npxArgs([ + '--yes', + '@react-native-community/cli@^20.2.0', + 'init', + appName, + '--version', + '0.87.1', + '--skip-install', + '--directory', + destination, + ...initArgs, + ]), + { shell: npxShell }, + ); + const packagePath = path.join(destination, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + fs.writeFileSync( + packagePath, + `${JSON.stringify(scaffoldDesktop(destination, packageJson), null, 2)}\n`, + ); + fs.copyFileSync( + path.join(templatesDir, 'app/App.tsx'), + path.join(destination, 'App.tsx'), + ); + console.log( + `Created standard React Native project ${appName} (iOS, Android, and GPUI desktop) in ${destination}. Run npm install, then npx react-native run-desktop.`, + ); + } catch (error) { + fs.rmSync(destination, { recursive: true, force: true }); + if (destinationExisted) fs.mkdirSync(destination, { recursive: true }); + throw error; + } +} + +export function createLibrary({ + libraryName, + type = 'rust', + destination, + moduleName: requestedModuleName, + packageName: requestedPackageName, +}) { + if ( + !libraryName || + !/^[A-Za-z][A-Za-z0-9_-]*$/u.test(libraryName) || + !['rust', 'cpp'].includes(type) + ) { + throw new Error( + 'create-library requires a valid name and rust or cpp type', + ); + } + destination = path.resolve(destination ?? libraryName); + if (fs.existsSync(destination) && fs.readdirSync(destination).length) { + throw new Error(`destination is not empty: ${destination}`); + } + const slug = libraryName + .replaceAll(/([a-z0-9])([A-Z])/gu, '$1-$2') + .toLowerCase() + .replaceAll(/[^a-z0-9-]/gu, '-'); + const snakeCase = value => + value + .replaceAll(/([a-z0-9])([A-Z])/gu, '$1_$2') + .replaceAll(/([A-Z]+)([A-Z][a-z])/gu, '$1_$2') + .toLowerCase(); + const words = libraryName.match(/[A-Z]?[a-z0-9]+|[A-Z]+/gu) ?? [libraryName]; + const moduleStem = words + .map(word => word[0].toUpperCase() + word.slice(1).toLowerCase()) + .join(''); + const moduleName = requestedModuleName ?? `RN${moduleStem}Module`; + if (!/^[A-Za-z][A-Za-z0-9]*$/u.test(moduleName)) { + throw new Error('--module-name must be a PascalCase identifier'); + } + const specName = `Native${moduleStem}`; + const cppProvider = `create_${slug.replaceAll('-', '_')}_module`; + const packageName = requestedPackageName ?? `react-native-${slug}`; + // Two names have to line up: `JSI.h` is the generated header, and + // the spec class is named after the spec file (`src/${specName}.ts`). The C++ + // template writes `RNModuleSpec` and `NativeModuleCxxSpec`, which the + // replacements below turn into exactly these two names. + const codegenName = `${moduleName}Spec`; + const templateRoot = path.join(templatesDir, `modules/${type}`); + fs.mkdirSync(destination, { recursive: true }); + const moduleFiles = + type === 'rust' + ? { + [`src/${specName}.ts`]: 'NativeModule.ts', + 'src/index.ts': 'index.ts', + 'desktop/build.rs': 'build.rs', + 'desktop/src/lib.rs': 'lib.rs', + } + : { + [`src/${specName}.ts`]: 'NativeModule.ts', + 'src/index.ts': 'index.ts', + [`desktop/cpp/${moduleName}.hpp`]: 'RNModule.hpp', + [`desktop/cpp/${moduleName}.cpp`]: 'RNModule.cpp', + 'desktop/CMakeLists.txt': 'CMakeLists.txt', + }; + for (const [target, source] of Object.entries(moduleFiles)) { + const targetPath = path.join(destination, target); + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + const contents = fs + .readFileSync(path.join(templateRoot, source), 'utf8') + .replaceAll('RNMODULESNAKE', snakeCase(moduleStem)) + .replaceAll('NativeModule', specName) + .replaceAll('RNMODULE', moduleName.toUpperCase()) + .replaceAll('RNModule', moduleName) + .replaceAll('create_rn_module', cppProvider); + fs.writeFileSync(targetPath, contents); + } + + const platformConfig = + type === 'rust' + ? { crate: `rn-gpui-${slug}` } + : { cmake: 'desktop', cppProvider }; + const packageJson = { + name: packageName, + version: '0.1.0', + main: 'src/index.ts', + files: ['src', 'desktop', 'react-native.config.js', 'README.md'], + codegenConfig: { + name: codegenName, + type: 'modules', + jsSrcsDir: 'src', + }, + peerDependencies: { 'react-native': '>=0.76' }, + }; + fs.writeFileSync( + path.join(destination, 'package.json'), + `${JSON.stringify(packageJson, null, 2)}\n`, + ); + fs.writeFileSync( + path.join(destination, 'react-native.config.js'), + [ + 'module.exports = {', + ' dependency: {', + ' platforms: {', + ' desktop: {', + ...Object.entries(platformConfig).map( + ([key, value]) => ` ${key}: ${JSON.stringify(value)},`, + ), + ' },', + ' },', + ' },', + '};', + '', + ].join('\n'), + ); + if (type === 'rust') { + // Version requirements, not paths: `[patch.crates-io]` cannot redirect a + // `path` dependency, and a module installed from npm has no + // `../../../packages` above it. Autolink patches these to the core the app + // actually installed. + fs.writeFileSync( + path.join(destination, 'desktop/Cargo.toml'), + `[package]\nname = ${JSON.stringify(`rn-gpui-${slug}`)}\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nrn-gpui-module = "0.1"\n\n[build-dependencies]\nrn-gpui-module-build = "0.1"\n`, + ); + } + const exampleRoot = path.join(destination, 'example'); + fs.mkdirSync(exampleRoot, { recursive: true }); + fs.writeFileSync( + path.join(exampleRoot, 'app.json'), + `${JSON.stringify({ name: `${moduleStem}Example`, displayName: `${moduleStem} Example` }, null, 2)}\n`, + ); + const examplePackage = scaffoldDesktop(exampleRoot, { + name: `${slug}-example`, + version: '0.0.1', + private: true, + dependencies: { + react: '19.2.3', + 'react-native': '0.87.1', + [packageName]: 'file:..', + }, + devDependencies: { + '@react-native-community/cli': '^20.2.0', + '@react-native/babel-preset': '0.87.1', + '@react-native/metro-config': '0.87.1', + vitest: '^3.2.4', + }, + }); + fs.writeFileSync( + path.join(exampleRoot, 'package.json'), + `${JSON.stringify(examplePackage, null, 2)}\n`, + ); + fs.writeFileSync( + path.join(exampleRoot, 'babel.config.cjs'), + "module.exports = {presets: ['module:@react-native/babel-preset']};\n", + ); + fs.writeFileSync( + path.join(exampleRoot, 'index.ts'), + "import {AppRegistry} from 'react-native';\nimport App from './App';\nimport {name} from './app.json';\nAppRegistry.registerComponent(name, () => App);\n", + ); + fs.writeFileSync( + path.join(exampleRoot, 'App.tsx'), + `import React from 'react';\nimport {Text, View} from 'react-native';\nimport Module from ${JSON.stringify(packageName)};\nexport default function App() {\n return {Module.greet('Desktop')};\n}\n`, + ); + fs.writeFileSync( + path.join(exampleRoot, 'desktop/e2e/library.e2e.ts'), + "import {launchApp, expect as desktopExpect} from '@react-native-gpui/testing';\ntest('calls the native library', async () => {\n const app = await launchApp({disableAnimations: true});\n await desktopExpect(app.getByTestId('library-result')).toContainText('Desktop');\n});\n", + ); + + const language = type === 'rust' ? 'Rust' : 'C++'; + const implementation = + type === 'rust' ? 'desktop/src/lib.rs' : `desktop/cpp/${moduleName}.cpp`; + const description = + type === 'rust' + ? 'call its typed greet(name) method.' + : 'implement methods defined by its generated spec.'; + fs.writeFileSync( + path.join(destination, 'README.md'), + `# ${libraryName}\n\n${language} TurboModule library for React Native GPUI. The spec is in src/${specName}.ts and uses React Native's public TurboModuleRegistry API. The implementation is in ${implementation} and is discovered through package metadata. Import the default export from ${packageName} and ${description}\n`, + ); + console.log( + `Created ${type} TurboModule library ${packageName} in ${destination}. Install/link it in a React Native app, then run npx react-native run-desktop.`, + ); +} + +/** + * The `_requireNodeLibrary(key, path)` calls the Babel plugin emits, as they + * appear in a bundle. Babel's output quotes both arguments with double quotes + * and escapes anything unusual, so this matches the compact and pretty forms + * alike. The key never contains a quote or a backslash (see the plugin's + * `addonKey`), which is what keeps the pattern simple. + */ +const requireNodeLibraryCallPattern = + /_requireNodeLibrary\(\s*"([^"\\]+)"\s*,\s*"((?:[^"\\]|\\.)+)"\s*\)/gu; + +/** + * The Node addon entries a bundle asks for: `{ key, path }`, in the order the + * calls appear, with the escapes Babel produced removed from the path. + * + * Exported for the unit test: scanning is the part worth testing without + * touching the filesystem. + */ +export function scanNodeAddonCalls(bundleText) { + const found = []; + const seen = new Set(); + for (const match of bundleText.matchAll(requireNodeLibraryCallPattern)) { + const key = match[1]; + // The path was emitted by `t.stringLiteral`, so it is escaped the way a JS + // string literal is. `JSON.parse` undoes exactly that. + let filePath; + try { + filePath = JSON.parse(`"${match[2]}"`); + } catch { + continue; + } + if (seen.has(key)) continue; + seen.add(key); + found.push({ key, path: filePath }); + } + return found; +} + +/** Sibling shared libraries a `$ORIGIN` / `@loader_path` lookup may need. */ +function isSiblingSharedLibrary(name, platform) { + if (platform === 'darwin') return name.endsWith('.dylib'); + if (platform === 'win32') return name.endsWith('.dll'); + // Linux: `libfoo.so` and its versioned forms `libfoo.so.1`, `libfoo.so.1.2`. + return /\.so(?:\.\d+)*$/u.test(name); +} + +/** + * `react-native-gpui collect-node-addons --bundle --out ` + * + * A development build loads an addon from the absolute path the Babel plugin + * embedded. A packaged app has no such path, so it copies each addon to + * `/` — which is what `_requireNodeLibrary` looks for relative to the + * executable — together with any sibling shared libraries, since some addons + * load those through `$ORIGIN` / `@loader_path`. + * + * Fails loudly when an embedded path no longer exists: a bundle built against a + * stale tree would otherwise ship without the addon and fail at run time. + */ +export function collectNodeAddons({ + bundle, + out, + platform = process.platform, +} = {}) { + if (!bundle) throw new Error('collect-node-addons requires --bundle '); + if (!out) throw new Error('collect-node-addons requires --out '); + const bundlePath = path.resolve(bundle); + const outDirectory = path.resolve(out); + const bundleText = fs.readFileSync(bundlePath, 'utf8'); + const addons = scanNodeAddonCalls(bundleText); + if (addons.length === 0) { + // A bundle with no addons removes a stale directory rather than leaving one + // from an earlier build behind. + fs.rmSync(outDirectory, { recursive: true, force: true }); + console.log(`No Node addons in ${bundlePath}; removed ${outDirectory}.`); + return { copied: [], out: outDirectory }; + } + + const missing = addons.filter(addon => !fs.existsSync(addon.path)); + if (missing.length > 0) { + throw new Error( + `collect-node-addons: the bundle names ${missing.length} addon file(s) ` + + 'that no longer exist. Rebuild the bundle before packaging:\n' + + missing.map(addon => ` ${addon.key} -> ${addon.path}`).join('\n'), + ); + } + + // Rebuilt from scratch so an addon that disappeared is not shipped. + fs.rmSync(outDirectory, { recursive: true, force: true }); + const copied = []; + for (const addon of addons) { + const target = path.join(outDirectory, addon.key); + if (!target.startsWith(outDirectory + path.sep)) { + throw new Error( + `collect-node-addons: key '${addon.key}' escapes ${outDirectory}`, + ); + } + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(addon.path, target); + copied.push({ key: addon.key, path: addon.path, target }); + + // Sibling shared libraries of the same directory, copied once per + // directory so two addons in one folder do not copy them twice. + for (const sibling of fs.readdirSync(path.dirname(addon.path))) { + if (sibling === path.basename(addon.path)) continue; + if (!isSiblingSharedLibrary(sibling, platform)) continue; + const source = path.join(path.dirname(addon.path), sibling); + if (!fs.statSync(source).isFile()) continue; + const siblingKey = path.join(path.dirname(addon.key), sibling); + const siblingTarget = path.join(outDirectory, siblingKey); + if (!siblingTarget.startsWith(outDirectory + path.sep)) continue; + if (fs.existsSync(siblingTarget)) continue; + fs.mkdirSync(path.dirname(siblingTarget), { recursive: true }); + fs.copyFileSync(source, siblingTarget); + copied.push({ key: siblingKey, path: source, target: siblingTarget }); + } + } + + for (const entry of copied) { + console.log(` ${entry.key} <- ${entry.path}`); + } + console.log( + `Copied ${addons.length} Node addon(s) and ${copied.length - addons.length} sibling librar(ies) to ${outDirectory}.`, + ); + return { copied, out: outDirectory }; +} + +/// The first four bytes of a Hermes bytecode file. +/// +/// Hermes identifies its output by content, and React Native's loader does too: +/// the file extension is not consulted, which is why a `.bundle` holding +/// bytecode works. The magic is `c6 1f bc 03`, followed by the version. +const HERMES_MAGIC = Buffer.from([0xc6, 0x1f, 0xbc, 0x03]); + +/** + * Strip the symbol table from the release binary, once its debug info is safe. + * + * Cargo's own `strip = "symbols"` runs inside the linker, before `dsymutil`, so + * it leaves an empty dSYM. Stripping here instead — after the dSYM is written and + * before the packager copies the binary — gets both: a 10 MB smaller binary and + * a debug file that can symbolize a crash. + */ +function stripReleaseBinary({ crateDir, binaryName }) { + const binary = path.join(targetProfileDir(crateDir, 'release'), binaryName); + if (!fs.existsSync(binary)) return; + try { + if (process.platform === 'win32') return; // The linker already omits it. + // `-x` removes every symbol that is not exported, which is the 10 MB the + // symbol table costs. `-S` only removes debug info and leaves the table. + execFileSync('strip', ['-x', binary], { stdio: 'inherit' }); + console.log(`stripped symbols from ${path.basename(binary)}`); + } catch { + console.log( + `could not strip ${path.basename(binary)}; the package will be larger`, + ); + } +} + +/** + * Copy the bundle a release should ship to `desktop/build/package/index.bundle`. + * + * One name, one file, and the content is whatever the engine produced. A + * Hermes build ships bytecode; a JavaScript-only engine ships JavaScript under + * the same name, with one line saying so. The name never varies, so the runtime + * has nothing to guess and `Resources/` holds exactly one bundle. + */ +function stageBundle({ projectRoot, build }) { + const packageDir = path.join(projectRoot, 'desktop', 'build', 'package'); + fs.mkdirSync(packageDir, { recursive: true }); + const staged = path.join(packageDir, 'index.bundle'); + const source = build?.runtimeBundlePath; + if (typeof source !== 'string' || !fs.existsSync(source)) { + throw new Error( + `the release bundle was not produced: expected ${source ?? '(no path)'}`, + ); + } + fs.copyFileSync(source, staged); + + const head = Buffer.alloc(HERMES_MAGIC.length); + const fd = fs.openSync(staged, 'r'); + try { + fs.readSync(fd, head, 0, HERMES_MAGIC.length, 0); + } finally { + fs.closeSync(fd); + } + if (head.equals(HERMES_MAGIC)) return staged; + + // Not bytecode. That is correct for a JavaScript-only engine and a packaging + // mistake with Hermes, so the two are told apart by whether the engine claims + // to compile at all. + const engine = loadEngineBundle(projectRoot); + if (engine && typeof engine.compile === 'function') { + throw new Error( + `the Hermes engine did not produce bytecode: ${source} starts with ` + + `${JSON.stringify(head.toString('latin1'))}, not the Hermes magic`, + ); + } + console.log( + 'packaging a JavaScript bundle: the active engine compiles to bytecode only with Hermes', + ); + return staged; +} + +/// The `name` a desktop crate declares in its Cargo.toml. +function desktopCrateName(crateDir) { + const manifest = fs.readFileSync(path.join(crateDir, 'Cargo.toml'), 'utf8'); + const match = manifest.match(/^name\s*=\s*"([^"]+)"/mu); + if (!match) { + throw new Error(`no package name in ${path.join(crateDir, 'Cargo.toml')}`); + } + return match[1]; +} + +/** + * `package-desktop`: turn the app into installable packages for this OS. + * + * Always a release build. The steps are: load the config, produce the release + * bundle and binary the way `run-desktop --release` does, translate the config + * into cargo-packager's, and run it. cargo-packager is an implementation + * detail — nothing here writes its format by hand. + */ +export async function packageDesktop({ + projectRoot = process.cwd(), + targets, + outDir, +} = {}) { + projectRoot = path.resolve(projectRoot); + const config = loadDesktopConfig(projectRoot, process.platform); + // `--target` reaches the command as the string after the flag. + const requested = + targets === undefined || targets === true + ? config + : { + ...config, + bundle: { + ...config.bundle, + targets: String(targets) + .split(',') + .map(format => format.trim()) + .filter(Boolean), + }, + }; + const formats = targetsFor(requested, process.platform); + + // 1. The release bundle and binary. `packaging` makes build.rs emit only + // relative rpaths, so the result carries no path into this build tree. + const build = await runDesktop({ + projectRoot, + release: true, + noLaunch: true, + packaging: true, + }); + // What the release ships: one bundle, always named `index.bundle`, holding + // whatever the engine compiled the JavaScript into. React Native's own release + // builds do the same — an iOS `main.jsbundle` is bytecode when Hermes is on. + const shippedBundle = stageBundle({ projectRoot, build }); + + const crateDir = path.join(projectRoot, 'desktop'); + // The executable is the crate's name, which is what `scaffoldDesktop` writes + // into Cargo.toml and what `cargo build` produces. + const binaryName = desktopCrateName(crateDir); + const buildDir = path.join(crateDir, 'build'); + + // The binary is finished. Ask for the debug file first — it can only be + // collected from a binary that still has its debug info — then take the symbols + // off, so the packager copies the small binary. + ensureDsym({ crateDir, binaryName }); + stripReleaseBinary({ crateDir, binaryName }); + + // 2. Icons, from the one PNG the config names. + const icons = generateIcons(config, { crateDir, projectRoot }); + + // 3. The libraries that have to travel with the app. + const libraries = runtimeLibraries(projectRoot, { + os: process.platform, + arch: process.arch, + release: true, + }); + // Named, because a library left out of the package is otherwise invisible until + // the installer or the AppImage step fails with a message that does not say + // which file it was looking for. + for (const library of libraries) { + console.log(`shipping library ${path.basename(library)}`); + } + + // 4. Translate, then run cargo-packager. + // The rpath `build.rs` emits names the app, not the crate: it is + // `RN_GPUI_APP_NAME`, which comes from `app.json`. The deb has to install its + // libraries into that same directory or the binary cannot find them. + const appJson = JSON.parse( + fs.readFileSync(path.join(projectRoot, 'app.json'), 'utf8'), + ); + const packagerConfig = toPackagerConfig(config, { + crateDir, + binaryName, + rpathDir: appJson.name, + os: desktopOs(process.platform), + bundleDir: path.dirname(shippedBundle), + assetsDir: path.join(buildDir, 'assets'), + icons, + libraries, + formats, + ...(outDir === undefined ? {} : { outDir: path.resolve(outDir) }), + }); + // The config is written at the crate root, not in `build/`: cargo-packager + // resolves every path in it — the binary, the resources, the frameworks — + // relative to the config file's own directory, so a config one level down + // would look for `build/build/index.bundle`. + const configPath = path.join(crateDir, 'packager.json'); + fs.mkdirSync(buildDir, { recursive: true }); + fs.writeFileSync(configPath, `${JSON.stringify(packagerConfig, null, 2)}\n`); + + ensureCargoPackager(); + run( + 'cargo', + ['packager', '--config', 'packager.json', '--formats', formats.join(',')], + { cwd: crateDir }, + ); + + fs.rmSync(configPath, { force: true }); + const produced = listPackages(packagerConfig.outDir); + // After the packages, not before: cargo-packager writes `dist` itself and would + // remove anything already there. + copyDebugSymbols({ crateDir, binaryName, outDir: packagerConfig.outDir }); + for (const file of produced) console.log(file); + return produced; +} + +/** + * Installs cargo-packager when the pinned version is not the one on PATH. + * + * One line saying so: the install takes a few minutes and happens once. + */ +export function ensureCargoPackager() { + let installed; + try { + installed = execFileSync('cargo', ['packager', '--version'], { + encoding: 'utf8', + }).trim(); + } catch { + installed = ''; + } + if (installed.includes(CARGO_PACKAGER_VERSION)) return; + console.log( + `installing cargo-packager ${CARGO_PACKAGER_VERSION} (once, a few minutes)`, + ); + run( + 'cargo', + [ + 'install', + 'cargo-packager', + '--locked', + '--version', + CARGO_PACKAGER_VERSION, + ], + {}, + ); +} + +/** + * Write the release dSYM if it is not there yet. + * + * Cargo does not invoke `dsymutil`, so a binary built with `strip = "none"` still + * has no dSYM until one is asked for. This must happen before `stripReleaseBinary` + * — the debug info has to be in the binary to be collected out of it. + */ +function ensureDsym({ crateDir, binaryName }) { + const profileDir = targetProfileDir(crateDir, 'release'); + const binary = path.join(profileDir, binaryName); + const dsym = path.join(profileDir, `${binaryName}.dSYM`); + if (!fs.existsSync(binary)) return; + // `dsymutil` writes the bundle read-only, and Cargo then cannot replace the + // binary it belongs to: "failed to remove file … Operation not permitted". Make + // it writable before removing it. + if (fs.existsSync(dsym)) fs.chmodSync(dsym, 0o755); + fs.rmSync(dsym, { recursive: true, force: true }); + try { + execFileSync('dsymutil', [binary, '-o', dsym], { stdio: 'inherit' }); + // `dsymutil` writes the bundle read-only, and Cargo then cannot replace the + // binary it belongs to: "failed to remove file … Operation not permitted". + if (fs.existsSync(dsym)) fs.chmodSync(dsym, 0o755); + } catch (error) { + console.log(`could not write a dSYM for ${binaryName}: ${error.message}`); + } +} + +/// Whether `file` is a directory with no files anywhere below it. +function isEmptyDirectory(file) { + const stat = fs.statSync(file); + if (!stat.isDirectory()) return false; + const stack = [file]; + while (stack.length > 0) { + const directory = stack.pop(); + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + if (entry.isFile()) return false; + if (entry.isDirectory()) stack.push(path.join(directory, entry.name)); + } + } + return true; +} + +/** + * Put the release debug file beside the package, and not inside it. + * + * The binary is stripped of its symbol table, so a crash report is addresses and + * nothing else. The dSYM (or `.dwp`, or `.pdb`) is what turns those back into + * function names, and it must not ship: it is larger than the binary and useless + * to anyone who is not symbolizing. + */ +function copyDebugSymbols({ crateDir, binaryName, outDir }) { + const profileDir = targetProfileDir(crateDir, 'release'); + // `split-debuginfo = "packed"` gives a dSYM on macOS, a `.dwp` on Linux and a + // `.pdb` on Windows. The unpacked `.debug` file is listed too because a + // toolchain that does not support packing falls back to it. + const candidates = [ + path.join(profileDir, `${binaryName}.dSYM`), + path.join(profileDir, `${binaryName}.dwp`), + path.join(profileDir, `${binaryName}.pdb`), + path.join(profileDir, `${binaryName}.debug`), + ]; + // A dSYM directory that exists but holds no DWARF is a leftover, not a result. + const found = candidates.filter( + file => fs.existsSync(file) && !isEmptyDirectory(file), + ); + if (found.length === 0) { + console.log( + 'no release debug file was produced; a crash report will not be symbolizable', + ); + return; + } + for (const file of found) { + const target = path.join(outDir, path.basename(file)); + fs.rmSync(target, { recursive: true, force: true }); + fs.cpSync(file, target, { recursive: true }); + // A read-only directory here would make the next run fail to replace it. + fs.chmodSync(target, 0o755); + console.log(`debug symbols: ${target}`); + } +} + +/** The packages a run produced, newest first. */ +function listPackages(outDir) { + let entries; + try { + entries = fs.readdirSync(outDir, { withFileTypes: true }); + } catch { + return []; + } + return entries + .filter(entry => entry.isFile()) + .map(entry => path.join(outDir, entry.name)) + .sort(); +} + +/** + * The icon a package builds from: the one `desktop.json` names, or the CLI's + * placeholder. + * + * cargo-packager derives a macOS `.icns` entry from an icon's pixel size times + * its density, and only a handful of those pairs are ICNS types. A 1024x1024 + * PNG at 1x is not one of them, so a single file fails with "No matching + * IconType" — which is what stopped the app bundle as well as the AppImage. + * Naming the copy `@2x` says what the file is: a 1024px asset for a 512pt slot, + * which is a valid type, and every other format is derived from it. + * + * The `@2x` convention is cargo-packager's own, and the copy is why no image + * library is needed: the CLI never rescales a user's icon, it labels it. + * + * There is always an icon: an app that never set `icon` gets the placeholder, + * because the AppImage step refuses to build without a square one. + */ +function generateIcons(config, { crateDir }) { + const source = + config.icon === undefined + ? defaultIconPath() + : path.resolve(crateDir, config.icon); + if (!fs.existsSync(source)) { + throw new Error( + `desktop.json names an icon that does not exist: ${source}`, + ); + } + + const iconDir = path.join(crateDir, 'build', 'icons'); + fs.mkdirSync(iconDir, { recursive: true }); + const target = path.join(iconDir, `icon@2x${path.extname(source)}`); + if (path.resolve(source) !== path.resolve(target)) { + fs.copyFileSync(source, target); + } + return [path.relative(crateDir, target).split(path.sep).join('/')]; +} + +/** The icon `desktop.json` names, or the CLI's placeholder. */ +function appIconPath(projectRoot) { + const { icon } = loadDesktopConfig(projectRoot, process.platform); + const source = + icon === undefined + ? defaultIconPath() + : path.resolve(projectRoot, 'desktop', icon); + return fs.existsSync(source) ? source : defaultIconPath(); +} + +/// The square PNG bundled with the CLI, used when an app sets no icon. +function defaultIconPath() { + return path.join(cliDir, '..', 'assets', 'default-icon.png'); +} diff --git a/vendor/react-native-gpui/packages/cli/src/commands.test.js b/vendor/react-native-gpui/packages/cli/src/commands.test.js new file mode 100644 index 0000000000..6296f93f3a --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/commands.test.js @@ -0,0 +1,469 @@ +import fs from 'node:fs'; +import http from 'node:http'; +import nodePath from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterAll, expect, test } from 'vitest'; +import { + collectNodeAddons, + createLibrary, + desktopLaunchConfig, + linuxBuildDependencyProblem, + metroProjectRoot, + metroTerminal, + scanNodeAddonCalls, + scaffoldDesktop, +} from './commands.js'; + +// The repository root by lookup, not by counting parents: this file sits at a +// different depth before and after a folder move. +const repoRoot = (() => { + let dir = nodePath.dirname(fileURLToPath(import.meta.url)); + for (;;) { + const manifest = nodePath.join(dir, 'package.json'); + if (fs.existsSync(manifest)) { + try { + if (JSON.parse(fs.readFileSync(manifest, 'utf8')).workspaces) + return dir; + } catch {} + } + const parent = nodePath.dirname(dir); + if (parent === dir) throw new Error('no workspace root above the test'); + dir = parent; + } +})(); +const temporaryRoots = []; + +afterAll(() => { + for (const root of temporaryRoots) { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test('the Linux preflight accepts a complete system', () => { + expect(linuxBuildDependencyProblem('linux', () => true)).toBeNull(); +}); + +test('the Linux preflight names the missing module and its package', () => { + const problem = linuxBuildDependencyProblem('linux', pc => pc !== 'xcb'); + expect(problem).toContain('dependency xcb was not found'); + expect(problem).toContain('libxcb1-dev'); + expect(problem).toContain('PKG_CONFIG_PATH'); +}); + +test('the Linux preflight gives one package name per distribution', () => { + const problem = linuxBuildDependencyProblem( + 'linux', + pc => pc !== 'xkbcommon', + ); + expect(problem).toContain('Debian/Ubuntu: sudo apt install libxkbcommon-dev'); + expect(problem).toContain('Fedora: sudo dnf install libxkbcommon-devel'); + expect(problem).toContain('Arch: sudo pacman -S libxkbcommon'); +}); + +test('the Linux preflight prints the computed system pkg-config path', () => { + const path = '/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig'; + const problem = linuxBuildDependencyProblem( + 'linux', + () => false, + () => path, + ); + expect(problem).toContain(`PKG_CONFIG_PATH=${path}`); +}); + +test('the Linux preflight falls back when the system path is unknown', () => { + const problem = linuxBuildDependencyProblem( + 'linux', + () => false, + () => null, + ); + expect(problem).not.toContain('PKG_CONFIG_PATH='); + expect(problem).toContain('point PKG_CONFIG_PATH at your system pkgconfig'); +}); + +test('the Linux preflight reports a missing pkg-config', () => { + expect( + linuxBuildDependencyProblem('linux', () => 'missing-pkg-config'), + ).toContain('pkg-config is required'); +}); + +test('the Linux preflight is a no-op on other platforms', () => { + expect(linuxBuildDependencyProblem('darwin', () => false)).toBeNull(); + expect(linuxBuildDependencyProblem('win32', () => false)).toBeNull(); +}); + +// --- collect-node-addons ----------------------------------------------------- + +test('the scanner reads every key/path pair a bundle emits', () => { + const bundle = [ + 'var a = _requireNodeLibrary("node_modules/pkg/prebuilds/x.node", "/app/node_modules/pkg/prebuilds/x.node");', + 'var b = _requireNodeLibrary(\n "local/deadbeef/y.node",\n "/app/build/Release/y.node"\n);', + ].join('\n'); + expect(scanNodeAddonCalls(bundle)).toEqual([ + { + key: 'node_modules/pkg/prebuilds/x.node', + path: '/app/node_modules/pkg/prebuilds/x.node', + }, + { key: 'local/deadbeef/y.node', path: '/app/build/Release/y.node' }, + ]); +}); + +test('the scanner reports one entry per key', () => { + const bundle = + '_requireNodeLibrary("a/x.node","/p/x.node");_requireNodeLibrary("a/x.node","/p/x.node");'; + expect(scanNodeAddonCalls(bundle)).toHaveLength(1); +}); + +test('the scanner ignores a call with only a key', () => { + expect(scanNodeAddonCalls('_requireNodeLibrary("a/x.node")')).toEqual([]); +}); + +test('the scanner unescapes the path Babel emitted', () => { + const bundle = String.raw`_requireNodeLibrary("local/1/a.node","C:\\app\\a.node");`; + expect(scanNodeAddonCalls(bundle)[0].path).toBe('C:\\app\\a.node'); +}); + +test('collect-node-addons copies each addon to /', () => { + const root = fs.mkdtempSync(nodePath.join(repoRoot, '.local/collect-')); + temporaryRoots.push(root); + const addonPath = nodePath.join(root, 'build/Release/addon.node'); + fs.mkdirSync(nodePath.dirname(addonPath), { recursive: true }); + fs.writeFileSync(addonPath, 'binary'); + const bundlePath = nodePath.join(root, 'bundle.js'); + // The Babel plugin emits the path through `t.stringLiteral`, so it is escaped + // as a JS string literal would be — backslashes and all on Windows. + fs.writeFileSync( + bundlePath, + `_requireNodeLibrary("local/1/addon.node",${JSON.stringify(addonPath)});`, + ); + const out = nodePath.join(root, 'node-addons'); + const result = collectNodeAddons({ bundle: bundlePath, out }); + expect(result.copied).toHaveLength(1); + expect( + fs.readFileSync(nodePath.join(out, 'local/1/addon.node'), 'utf8'), + ).toBe('binary'); +}); + +test('collect-node-addons copies sibling shared libraries', () => { + const root = fs.mkdtempSync(nodePath.join(repoRoot, '.local/collect-')); + temporaryRoots.push(root); + const directory = nodePath.join(root, 'build/Release'); + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync(nodePath.join(directory, 'addon.node'), 'binary'); + fs.writeFileSync(nodePath.join(directory, 'libdep.so'), 'shared'); + fs.writeFileSync(nodePath.join(directory, 'libdep.so.1'), 'shared'); + fs.writeFileSync(nodePath.join(directory, 'unrelated.txt'), 'no'); + const bundlePath = nodePath.join(root, 'bundle.js'); + fs.writeFileSync( + bundlePath, + `_requireNodeLibrary("local/1/addon.node",${JSON.stringify(nodePath.join(directory, 'addon.node'))});`, + ); + const out = nodePath.join(root, 'node-addons'); + collectNodeAddons({ bundle: bundlePath, out, platform: 'linux' }); + expect(fs.existsSync(nodePath.join(out, 'local/1/libdep.so'))).toBe(true); + expect(fs.existsSync(nodePath.join(out, 'local/1/libdep.so.1'))).toBe(true); + expect(fs.existsSync(nodePath.join(out, 'local/1/unrelated.txt'))).toBe( + false, + ); +}); + +test('collect-node-addons fails when an embedded path is gone', () => { + const root = fs.mkdtempSync(nodePath.join(repoRoot, '.local/collect-')); + temporaryRoots.push(root); + const bundlePath = nodePath.join(root, 'bundle.js'); + fs.writeFileSync( + bundlePath, + '_requireNodeLibrary("local/1/gone.node","/nowhere/gone.node");', + ); + expect(() => + collectNodeAddons({ bundle: bundlePath, out: nodePath.join(root, 'out') }), + ).toThrow(/no longer exist/u); +}); + +test('collect-node-addons clears a stale directory when the bundle has none', () => { + const root = fs.mkdtempSync(nodePath.join(repoRoot, '.local/collect-')); + temporaryRoots.push(root); + const bundlePath = nodePath.join(root, 'bundle.js'); + fs.writeFileSync(bundlePath, 'var x = 1;'); + const out = nodePath.join(root, 'node-addons'); + fs.mkdirSync(out, { recursive: true }); + fs.writeFileSync(nodePath.join(out, 'stale'), 'x'); + const result = collectNodeAddons({ bundle: bundlePath, out }); + expect(result.copied).toEqual([]); + expect(fs.existsSync(out)).toBe(false); +}); + +test('collect-node-addons requires both arguments', () => { + expect(() => collectNodeAddons({})).toThrow(/--bundle/u); + expect(() => collectNodeAddons({ bundle: 'x' })).toThrow(/--out/u); +}); + +// The launch chain used to be built from `app.json`'s `desktop` key. It reads +// `desktop/desktop.json` now, and these check that the generated `main.rs` is +// the same for the same options. +test('menuBar: false asks the host for no menu bar, and its absence does not', () => { + const hidden = desktopLaunchConfig( + { window: { menuBar: false }, devMenu: true }, + 'desktop/desktop.json', + ); + expect(hidden).toContain('.menu_bar(false)'); + + // The default is a menu bar, so the call is only emitted when it is turned off: + // a chain that always emitted it would be the same thing, but the generated file + // reads better without a line saying what the default already is. + const shown = desktopLaunchConfig( + { window: { menuBar: true }, devMenu: true }, + 'desktop/desktop.json', + ); + expect(shown).not.toContain('.menu_bar('); +}); + +test('a custom title bar and the traffic light position reach the launch chain', () => { + const hidden = desktopLaunchConfig( + { + window: { + titleBar: 'hidden', + trafficLightPosition: { x: 14, y: 10 }, + }, + devMenu: true, + }, + 'desktop/desktop.json', + ); + expect(hidden).toContain( + '.title_bar(react_native_gpui::WindowTitleBar::Hidden)', + ); + expect(hidden).toContain('.traffic_light_position(14.0, 10.0)'); + + const transparent = desktopLaunchConfig( + { window: { titleBar: 'transparent' }, devMenu: true }, + 'desktop/desktop.json', + ); + expect(transparent).toContain( + '.title_bar(react_native_gpui::WindowTitleBar::Transparent)', + ); + + // `default` is what an app that says nothing already gets, so it must not add + // a call that only restates it. + const plain = desktopLaunchConfig( + { window: { titleBar: 'default' }, devMenu: true }, + 'desktop/desktop.json', + ); + expect(plain).not.toContain('.title_bar('); + expect(plain).not.toContain('.traffic_light_position('); +}); + +test('a traffic light position without both coordinates is rejected', () => { + // The schema says `x` and `y` are both required; a launcher that silently used + // the origin would move the traffic lights to the corner of the window. + expect(() => + desktopLaunchConfig( + { window: { trafficLightPosition: { x: 14 } }, devMenu: true }, + 'desktop/desktop.json', + ), + ).toThrow(/trafficLightPosition needs x and y/u); +}); + +test('the launch chain reflects the window, dev menu and tray', () => { + const main = desktopLaunchConfig( + { + window: { width: 900, height: 700, title: 'My App', resizable: true }, + devMenu: true, + tray: { component: 'TrayRoot', tooltip: 'Open', accessory: true }, + }, + 'desktop/desktop.json', + ); + expect(main).toContain('.window_title("My App")'); + expect(main).toContain('.window_size(900.0, 700.0)'); + expect(main).toContain('.dev_menu(true)'); + expect(main).toContain( + '.tray(TrayConfig::default().component("TrayRoot").tooltip("Open"))', + ); + expect(main).toContain('options.is_resizable = true;'); +}); + +test('an app asking to restore nothing and be fixed-size says so', () => { + const main = desktopLaunchConfig( + { + window: { + width: 400, + height: 300, + resizable: false, + restoreState: false, + position: { x: 10, y: 20 }, + }, + devMenu: false, + }, + 'desktop/desktop.json', + ); + expect(main).toContain('.restore_window_state(false)'); + expect(main).toContain('.window_position(10.0, 20.0)'); + expect(main).toContain('.dev_menu(false)'); + expect(main).toContain('options.is_resizable = false;'); + expect(main).not.toContain('.tray('); +}); + +test('a tray with accessory false disables the accessory mode', () => { + const main = desktopLaunchConfig( + { + window: { width: 1, height: 1 }, + devMenu: true, + tray: { accessory: false }, + }, + 'desktop/desktop.json', + ); + expect(main).toContain('.tray(TrayConfig::default().accessory(false))'); +}); + +test('create-library writes version requirements, not crate paths', () => { + // A module crate installed from npm has no `../../../packages` above it, so a + // relative path to core cannot work there; `[patch.crates-io]` also cannot + // redirect a `path` dependency. The scaffolder therefore writes a version + // requirement and lets autolink patch it to the installed core. + const outer = fs.mkdtempSync(nodePath.join(repoRoot, '.local', 'scaffold-')); + try { + createLibrary({ + libraryName: 'fs', + type: 'rust', + destination: outer, + packageName: '@react-native-gpui/fs', + }); + const manifest = fs.readFileSync( + nodePath.join(outer, 'desktop/Cargo.toml'), + 'utf8', + ); + expect(manifest).toContain('rn-gpui-module = "0.1"'); + expect(manifest).toContain('rn-gpui-module-build = "0.1"'); + expect(manifest).not.toMatch(/path\s*=/u); + expect(manifest).not.toMatch(/git\s*=/u); + } finally { + fs.rmSync(outer, { recursive: true, force: true }); + } +}); + +test('scaffoldDesktop writes the env file that exposes the desktop types', () => { + // The augmentation is global, and a TypeScript augmentation only applies when + // its declaring file is compiled. This reference is what puts it in the app's + // compilation, so `desktopDragRegion` and `onContextMenu` type-check without an + // import of the desktop entry point. + const root = fs.mkdtempSync(nodePath.join(repoRoot, '.local', 'envtypes-')); + try { + fs.writeFileSync( + nodePath.join(root, 'package.json'), + `${JSON.stringify({ name: 'envapp', dependencies: { 'react-native': '0.87.1' } })}\n`, + ); + fs.writeFileSync( + nodePath.join(root, 'app.json'), + `${JSON.stringify({ name: 'EnvApp', displayName: 'EnvApp' })}\n`, + ); + scaffoldDesktop( + root, + JSON.parse(fs.readFileSync(nodePath.join(root, 'package.json'), 'utf8')), + ); + + const envPath = nodePath.join(root, 'react-native-gpui-env.d.ts'); + const written = fs.readFileSync(envPath, 'utf8'); + expect(written).toContain( + '/// ', + ); + expect(written).toContain('Do not edit'); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test('scaffoldDesktop keeps an app’s edited env file', () => { + // `preserveExisting` is how `init-desktop` runs on an app that already has a + // desktop folder, so an app that added its own references there must keep them. + const root = fs.mkdtempSync(nodePath.join(repoRoot, '.local', 'envkeep-')); + try { + fs.writeFileSync( + nodePath.join(root, 'package.json'), + `${JSON.stringify({ name: 'keepapp', dependencies: { 'react-native': '0.87.1' } })}\n`, + ); + fs.writeFileSync( + nodePath.join(root, 'app.json'), + `${JSON.stringify({ name: 'KeepApp', displayName: 'KeepApp' })}\n`, + ); + const envPath = nodePath.join(root, 'react-native-gpui-env.d.ts'); + const edited = '/// \n'; + fs.writeFileSync(envPath, edited); + + scaffoldDesktop( + root, + JSON.parse(fs.readFileSync(nodePath.join(root, 'package.json'), 'utf8')), + { preserveExisting: true }, + ); + expect(fs.readFileSync(envPath, 'utf8')).toBe(edited); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test('Metro opens in the requested terminal, then the platform default', () => { + const none = () => false; + expect(metroTerminal('Warp', { platform: 'darwin', env: {} })).toBe('Warp'); + expect( + metroTerminal(undefined, { + platform: 'darwin', + env: { REACT_TERMINAL: 'Ghostty' }, + }), + ).toBe('Ghostty'); + expect( + metroTerminal(undefined, { + platform: 'darwin', + env: { TERM_PROGRAM: 'vscode' }, + }), + ).toBe('Terminal'); + expect( + metroTerminal(undefined, { + platform: 'darwin', + env: { TERM_PROGRAM: 'iTerm.app' }, + }), + ).toBe('iTerm'); + expect(metroTerminal(undefined, { platform: 'win32', env: {} })).toBe( + 'cmd.exe', + ); + expect( + metroTerminal(undefined, { + platform: 'linux', + env: {}, + exists: command => command === 'konsole', + }), + ).toBe('konsole'); + expect( + metroTerminal(undefined, { platform: 'linux', env: {}, exists: none }), + ).toBeUndefined(); +}); + +test('a running Metro reports the project it serves', async () => { + const server = http.createServer((request, response) => { + response.setHeader('X-React-Native-Project-Root', '/some/project'); + response.end( + request.url === '/status' ? 'packager-status:running' : 'not found', + ); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const { port } = server.address(); + try { + expect(await metroProjectRoot('127.0.0.1', port)).toBe('/some/project'); + } finally { + await new Promise(resolve => server.close(resolve)); + } + expect(await metroProjectRoot('127.0.0.1', port)).toBeUndefined(); +}); + +test('a tray icon in desktop.json is embedded in the launch chain', () => { + const chain = desktopLaunchConfig( + { + tray: { tooltip: 'Tray', icon: 'assets/tray.png', iconTemplate: true }, + devMenu: true, + }, + 'desktop/desktop.json', + ); + expect(chain).toContain( + '.icon_bytes(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/tray.png")))', + ); + expect(chain).toContain('.icon_template(true)'); + expect( + desktopLaunchConfig({ tray: { tooltip: 'Tray' }, devMenu: true }, 'x'), + ).not.toContain('.icon_bytes('); +}); diff --git a/vendor/react-native-gpui/packages/cli/src/desktop-config.js b/vendor/react-native-gpui/packages/cli/src/desktop-config.js new file mode 100644 index 0000000000..f8e70ecea0 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/desktop-config.js @@ -0,0 +1,397 @@ +/** + * `desktop/desktop.json` — the one declarative file that describes a desktop + * app: its name, identifier, version, icon, window, and how it is packaged. + * + * The model is Tauri's `tauri.conf.json`: the app owns the file, it is data + * rather than code, and it ships a JSON schema so editors can complete it. + * `react-native.config.js` keeps only build-tooling options, because it is + * executable JavaScript that nothing can read statically. + * + * Every field is optional. An app with no `desktop.json` still runs and packages + * with the defaults below. + */ +import fs from 'node:fs'; +import path from 'node:path'; + +/** The alpha formats each OS can produce. `all` expands to these. */ +export const TARGETS_BY_OS = { + macos: ['app', 'dmg'], + windows: ['nsis'], + linux: ['deb', 'appimage'], +}; + +/** Every format this phase can produce, on any OS. */ +export const ALL_TARGETS = ['app', 'dmg', 'nsis', 'deb', 'appimage']; + +/** + * The OS name this module's files and targets use, from a Node platform. + * + * `desktop.macos.json` rather than `desktop.darwin.json`: the file names the + * platform a person would, and `process.platform` uses Node's spelling. + */ +export function desktopOs(platform = process.platform) { + switch (platform) { + case 'darwin': + return 'macos'; + case 'win32': + return 'windows'; + default: + return platform; + } +} + +/** + * The key names that must never appear in a config file. + * + * Signing material comes from the environment (D6). A password, token or + * private key written into a file that gets committed and shipped is a leak + * whether or not it is still valid, so a config carrying one is refused rather + * than warned about. + */ +const SECRET_KEY = /password|secret|token|privateKey/iu; + +export const DEFAULT_WINDOW = { width: 1024, height: 768 }; +export const DEFAULT_MACOS_MINIMUM = '13.0'; + +/** + * RFC 7386 JSON Merge Patch. + * + * A `null` in `patch` removes the key, which is how a per-OS file turns off a + * value the base set: `{"bundle": {"macos": null}}`. Arrays and scalars replace + * wholesale; only objects merge. + */ +export function mergePatch(target, patch) { + if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) { + return patch; + } + const base = + typeof target === 'object' && target !== null && !Array.isArray(target) + ? target + : {}; + const result = { ...base }; + for (const [key, value] of Object.entries(patch)) { + if (value === null) { + delete result[key]; + } else { + result[key] = mergePatch(base[key], value); + } + } + return result; +} + +/** `com.example.my-app`, from an app name. */ +export function identifierFromName(name) { + return `com.example.${String(name) + .toLowerCase() + .replaceAll(/[^a-z0-9-]/gu, '-')}`; +} + +/** + * The `desktop.json` path for an app. + * + * It lives in the app's existing `desktop/` folder, beside the Rust crate it + * configures. + */ +export function desktopConfigPath(projectRoot) { + return path.join(projectRoot, 'desktop', 'desktop.json'); +} + +function readJson(file, label) { + let text; + try { + text = fs.readFileSync(file, 'utf8'); + } catch (error) { + if (error.code === 'ENOENT') return undefined; + throw error; + } + try { + return JSON.parse(text); + } catch (error) { + throw new Error(`${label} is not valid JSON: ${file}\n${error.message}`); + } +} + +/** + * Loads the configuration for one OS. + * + * Reads `desktop/desktop.json`, merges `desktop/desktop..json` over it, + * fills the defaults, validates, and refuses a config carrying a secret. Pure + * apart from those reads. + */ +export function loadDesktopConfig(projectRoot, platform) { + const os = desktopOs(platform); + const basePath = path.join(projectRoot, 'desktop', 'desktop.json'); + const osPath = path.join(projectRoot, 'desktop', `desktop.${os}.json`); + const appConfigPath = path.join(projectRoot, 'app.json'); + const packageJsonPath = path.join(projectRoot, 'package.json'); + + const base = readJson(basePath, 'desktop/desktop.json'); + if (base !== undefined) assertObject(base, 'desktop/desktop.json', '$'); + const override = readJson(osPath, `desktop/desktop.${os}.json`); + if (override !== undefined) { + assertObject(override, `desktop/desktop.${os}.json`, '$'); + } + + // The key that used to hold these options. It is rejected rather than read: + // the option would be ignored, and the app would silently get a default. + const appConfig = readJson(appConfigPath, 'app.json') ?? {}; + if ( + appConfig.desktop !== undefined && + appConfig.desktop !== null && + typeof appConfig.desktop === 'object' && + Object.keys(appConfig.desktop).length > 0 + ) { + throw new Error( + 'move `app.json` desktop.* into `desktop/desktop.json` (see the README)', + ); + } + + const merged = + base === undefined && override === undefined + ? {} + : mergePatch(base ?? {}, override ?? {}); + + // Secrets first: the schema rejects unknown keys, and "unknown key" is a + // misleading way to describe a password someone tried to commit. + assertNoSecrets(merged, '$'); + + const errors = []; + validateAgainst(merged, SCHEMA, '$', errors); + if (errors.length > 0) { + throw new Error( + `invalid desktop/desktop.json:\n${errors.map(error => ` ${error}`).join('\n')}`, + ); + } + + const packageJson = readJson(packageJsonPath, 'package.json') ?? {}; + return withDefaults(merged, { appConfig, packageJson }); +} + +function assertObject(value, label, jsonPath) { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error(`invalid ${label}: ${jsonPath} must be an object`); + } +} + +/** + * Fills every field an app may omit. + * + * `window.title` defaults to `productName` rather than the app name, so a + * title bar and the app's own name are the same string unless asked otherwise. + */ +function withDefaults(config, { appConfig, packageJson }) { + const productName = + config.productName ?? + appConfig.displayName ?? + appConfig.name ?? + packageJson.name ?? + 'App'; + const identifier = + config.identifier ?? identifierFromName(appConfig.name ?? productName); + const version = config.version ?? packageJson.version ?? '1.0.0'; + const window = { ...DEFAULT_WINDOW, ...(config.window ?? {}) }; + if (window.title === undefined) window.title = productName; + if (window.resizable === undefined) window.resizable = true; + if (window.restoreState === undefined) window.restoreState = true; + + const tray = + config.tray === undefined ? undefined : { accessory: true, ...config.tray }; + + const bundle = { ...(config.bundle ?? {}) }; + if (bundle.targets === undefined) bundle.targets = 'all'; + if (bundle.resources === undefined) bundle.resources = []; + bundle.macos = { + minimumSystemVersion: DEFAULT_MACOS_MINIMUM, + ...(bundle.macos ?? {}), + }; + if (bundle.macos.entitlements === undefined) bundle.macos.entitlements = null; + if (bundle.macos.infoPlist === undefined) bundle.macos.infoPlist = {}; + bundle.windows = bundle.windows ?? {}; + bundle.windows.nsis = { + installMode: 'currentUser', + ...(bundle.windows.nsis ?? {}), + }; + bundle.linux = bundle.linux ?? {}; + bundle.linux.deb = { depends: [], ...(bundle.linux.deb ?? {}) }; + bundle.linux.appimage = bundle.linux.appimage ?? {}; + + return { + productName, + identifier, + version, + ...(config.icon === undefined ? {} : { icon: config.icon }), + window, + devMenu: config.devMenu ?? true, + ...(tray === undefined ? {} : { tray }), + bundle, + }; +} + +/** The formats to build for one OS. */ +export function targetsFor(config, platform) { + const os = desktopOs(platform); + const available = TARGETS_BY_OS[os] ?? []; + const requested = config.bundle?.targets ?? 'all'; + const list = requested === 'all' ? available : requested; + const unknown = list.filter(target => !ALL_TARGETS.includes(target)); + if (unknown.length > 0) { + throw new Error( + `unknown bundle target(s) ${unknown.join(', ')}; expected one of ${ALL_TARGETS.join(', ')} or 'all'`, + ); + } + const unavailable = list.filter(target => !available.includes(target)); + if (unavailable.length > 0) { + throw new Error( + `${unavailable.join(', ')} cannot be built on ${os}; available targets are ${available.join(', ')}`, + ); + } + return list; +} + +/** + * Walks the merged config and refuses any key that names signing material. + * + * The values are not quoted in the error: printing a leaked secret into a build + * log is the same mistake twice. + */ +function assertNoSecrets(value, jsonPath) { + if (Array.isArray(value)) { + value.forEach((item, index) => + assertNoSecrets(item, `${jsonPath}[${index}]`), + ); + return; + } + if (typeof value !== 'object' || value === null) return; + for (const [key, child] of Object.entries(value)) { + if (SECRET_KEY.test(key)) { + throw new Error( + `desktop/desktop.json must not contain signing material: ${jsonPath}.${key}. ` + + 'Set it in the environment instead; see the README.', + ); + } + assertNoSecrets(child, `${jsonPath}.${key}`); + } +} + +/** + * Validates against `desktop.schema.json`. + * + * A small validator for exactly this schema, because the CLI ships with no + * dependencies and the plan asks before adding one. It reports the JSON path of + * the first problem at each location rather than only the first overall: a + * config with two typos should not need two runs to fix. + * + * The schema subset it implements is `type`, `enum`, `pattern`, `minLength`, + * `exclusiveMinimum`, `required`, `properties`, `additionalProperties`, + * `items`, `minItems`, `oneOf`. + */ +function typeMatches(value, type) { + switch (type) { + case 'object': + return ( + typeof value === 'object' && value !== null && !Array.isArray(value) + ); + case 'array': + return Array.isArray(value); + case 'string': + return typeof value === 'string'; + case 'number': + return typeof value === 'number' && Number.isFinite(value); + case 'boolean': + return typeof value === 'boolean'; + case 'null': + return value === null; + default: + return true; + } +} + +function validateAgainst(value, schema, jsonPath, errors) { + const types = + schema.type === undefined + ? undefined + : Array.isArray(schema.type) + ? schema.type + : [schema.type]; + if (types !== undefined && !types.some(type => typeMatches(value, type))) { + errors.push(`${jsonPath}: expected ${types.join(' or ')}`); + return; + } + + if (schema.enum !== undefined && !schema.enum.includes(value)) { + errors.push( + `${jsonPath}: expected one of ${schema.enum.map(item => JSON.stringify(item)).join(', ')}`, + ); + return; + } + if (schema.pattern !== undefined && typeof value === 'string') { + if (!new RegExp(schema.pattern, 'u').test(value)) { + errors.push(`${jsonPath}: does not match ${schema.pattern}`); + } + } + if (schema.minLength !== undefined && typeof value === 'string') { + if (value.length < schema.minLength) { + errors.push(`${jsonPath}: must not be empty`); + } + } + if (schema.exclusiveMinimum !== undefined && typeof value === 'number') { + if (value <= schema.exclusiveMinimum) { + errors.push( + `${jsonPath}: must be greater than ${schema.exclusiveMinimum}`, + ); + } + } + + if (schema.oneOf !== undefined) { + const matched = schema.oneOf.filter(branch => { + const branchErrors = []; + validateAgainst(value, branch, jsonPath, branchErrors); + return branchErrors.length === 0; + }); + if (matched.length !== 1) { + errors.push( + `${jsonPath}: matches ${matched.length} of ${schema.oneOf.length} allowed shapes, expected exactly one`, + ); + } + } + + if (Array.isArray(value)) { + if (schema.minItems !== undefined && value.length < schema.minItems) { + errors.push(`${jsonPath}: must have at least ${schema.minItems} item(s)`); + } + if (schema.items !== undefined) { + value.forEach((item, index) => + validateAgainst(item, schema.items, `${jsonPath}[${index}]`, errors), + ); + } + return; + } + + if (typeof value === 'object' && value !== null) { + for (const required of schema.required ?? []) { + if (value[required] === undefined) { + errors.push(`${jsonPath}: missing required key ${required}`); + } + } + const properties = schema.properties ?? {}; + for (const [key, child] of Object.entries(value)) { + const childPath = `${jsonPath}.${key}`; + if (properties[key] !== undefined) { + validateAgainst(child, properties[key], childPath, errors); + } else if (schema.additionalProperties === false) { + const known = Object.keys(properties).sort().join(', '); + errors.push(`${childPath}: unknown key; known keys are ${known}`); + } + } + } +} + +/** The schema, read once. `desktop.schema.json` is shipped and documented. */ +const SCHEMA = JSON.parse( + fs.readFileSync(new URL('../desktop.schema.json', import.meta.url), 'utf8'), +); + +/** The schema itself, for `--print-schema` and for callers that ship it. */ +export function desktopSchema() { + return SCHEMA; +} diff --git a/vendor/react-native-gpui/packages/cli/src/desktop-config.test.js b/vendor/react-native-gpui/packages/cli/src/desktop-config.test.js new file mode 100644 index 0000000000..67aa288760 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/desktop-config.test.js @@ -0,0 +1,308 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, test } from 'vitest'; +import { + identifierFromName, + loadDesktopConfig, + mergePatch, + targetsFor, +} from './desktop-config.js'; + +const roots = []; + +afterEach(() => { + while (roots.length > 0) { + fs.rmSync(roots.pop(), { recursive: true, force: true }); + } +}); + +/** + * A minimal app: `app.json` and `package.json` only. `files` adds or replaces + * files by path relative to the project root. + */ +function createProject({ appJson, packageJson, files = {} } = {}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-gpui-desktop-')); + roots.push(root); + fs.mkdirSync(path.join(root, 'desktop'), { recursive: true }); + fs.writeFileSync( + path.join(root, 'app.json'), + `${JSON.stringify( + appJson ?? { name: 'MyApp', displayName: 'My App' }, + null, + 2, + )}\n`, + ); + fs.writeFileSync( + path.join(root, 'package.json'), + `${JSON.stringify( + packageJson ?? { name: 'my-app', version: '2.3.4' }, + null, + 2, + )}\n`, + ); + for (const [relative, contents] of Object.entries(files)) { + const target = path.join(root, relative); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync( + target, + typeof contents === 'string' + ? contents + : JSON.stringify(contents, null, 2), + ); + } + return root; +} + +describe('defaults', () => { + test('an app with no desktop.json gets every default', () => { + const root = createProject(); + const config = loadDesktopConfig(root, 'macos'); + expect(config.productName).toBe('My App'); + expect(config.identifier).toBe('com.example.myapp'); + expect(config.version).toBe('2.3.4'); + expect(config.window).toEqual({ + width: 1024, + height: 768, + title: 'My App', + resizable: true, + restoreState: true, + }); + expect(config.devMenu).toBe(true); + expect(config.tray).toBeUndefined(); + expect(config.bundle.targets).toBe('all'); + expect(config.bundle.resources).toEqual([]); + expect(config.bundle.macos.minimumSystemVersion).toBe('13.0'); + expect(config.bundle.macos.entitlements).toBeNull(); + expect(config.bundle.macos.infoPlist).toEqual({}); + expect(config.bundle.windows.nsis.installMode).toBe('currentUser'); + expect(config.bundle.linux.deb.depends).toEqual([]); + }); + + test('the window title defaults to productName, not the app name', () => { + const root = createProject({ + files: { 'desktop/desktop.json': { productName: 'Shiny' } }, + }); + expect(loadDesktopConfig(root, 'macos').window.title).toBe('Shiny'); + }); + + test('an explicit title wins over productName', () => { + const root = createProject({ + files: { + 'desktop/desktop.json': { + productName: 'Shiny', + window: { title: 'Other' }, + }, + }, + }); + expect(loadDesktopConfig(root, 'macos').window.title).toBe('Other'); + }); + + test('an identifier is derived from the app name when absent', () => { + const root = createProject({ appJson: { name: 'My_App 2' } }); + expect(loadDesktopConfig(root, 'macos').identifier).toBe( + 'com.example.my-app-2', + ); + expect(identifierFromName('A B')).toBe('com.example.a-b'); + }); +}); + +describe('per-OS overrides', () => { + test('desktop..json is merged over the base', () => { + const root = createProject({ + files: { + 'desktop/desktop.json': { + productName: 'Base', + window: { width: 800, height: 600 }, + }, + 'desktop/desktop.macos.json': { window: { width: 1200 } }, + }, + }); + const macos = loadDesktopConfig(root, 'macos'); + expect(macos.productName).toBe('Base'); + expect(macos.window.width).toBe(1200); + // Untouched keys keep the base value. + expect(macos.window.height).toBe(600); + + // The override only applies to its own OS. + expect(loadDesktopConfig(root, 'linux').window.width).toBe(800); + }); + + test('a null removes a key, as RFC 7386 says', () => { + const root = createProject({ + files: { + 'desktop/desktop.json': { + tray: { component: 'MyTray', tooltip: 'Hi' }, + }, + 'desktop/desktop.linux.json': { tray: null }, + }, + }); + expect(loadDesktopConfig(root, 'macos').tray).toEqual({ + component: 'MyTray', + tooltip: 'Hi', + accessory: true, + }); + expect(loadDesktopConfig(root, 'linux').tray).toBeUndefined(); + }); + + test('mergePatch replaces arrays rather than concatenating them', () => { + expect( + mergePatch( + { bundle: { resources: ['a', 'b'] } }, + { bundle: { resources: ['c'] } }, + ), + ).toEqual({ bundle: { resources: ['c'] } }); + }); + + test('merging into a missing base fills from the override alone', () => { + expect(mergePatch(undefined, { a: { b: 1 } })).toEqual({ a: { b: 1 } }); + }); +}); + +describe('validation', () => { + test('an unknown key is refused with its JSON path', () => { + const root = createProject({ + files: { 'desktop/desktop.json': { windo: { width: 1 } } }, + }); + expect(() => loadDesktopConfig(root, 'macos')).toThrow( + /\$\.windo: unknown key; known keys are/, + ); + }); + + test('a wrong type names the offending path', () => { + const root = createProject({ + files: { 'desktop/desktop.json': { window: { width: 'wide' } } }, + }); + expect(() => loadDesktopConfig(root, 'macos')).toThrow( + /\$\.window\.width: expected number/, + ); + }); + + test('a nested unknown key is caught too', () => { + const root = createProject({ + files: { 'desktop/desktop.json': { bundle: { macos: { nope: 1 } } } }, + }); + expect(() => loadDesktopConfig(root, 'macos')).toThrow( + /\$\.bundle\.macos\.nope: unknown key/, + ); + }); + + test('two problems are both reported', () => { + const root = createProject({ + files: { + 'desktop/desktop.json': { + window: { width: 'wide' }, + version: 7, + }, + }, + }); + expect(() => loadDesktopConfig(root, 'macos')).toThrow( + /window\.width: expected number[\s\S]*\$\.version: expected string/, + ); + }); + + test('a bad target is refused with the allowed list', () => { + const root = createProject({ + files: { 'desktop/desktop.json': { bundle: { targets: ['msi'] } } }, + }); + expect(() => loadDesktopConfig(root, 'macos')).toThrow( + /\$\.bundle\.targets: (matches 0 of|unknown bundle target)/, + ); + }); + + test('a target for another OS is refused by targetsFor, with what is available', () => { + // The schema allows any known format, because which ones an OS can build is + // a fact about the OS rather than about the file. + const root = createProject({ + files: { 'desktop/desktop.json': { bundle: { targets: ['nsis'] } } }, + }); + const config = loadDesktopConfig(root, 'macos'); + expect(() => targetsFor(config, 'macos')).toThrow( + /nsis cannot be built on macos; available targets are app, dmg/, + ); + }); + + test('a file that is not JSON says so', () => { + const root = createProject({ + files: { 'desktop/desktop.json': '{ nope' }, + }); + expect(() => loadDesktopConfig(root, 'macos')).toThrow( + /desktop\/desktop\.json is not valid JSON/, + ); + }); +}); + +describe('secrets', () => { + test.each([ + ['a password', { bundle: { macos: { password: 'hunter2' } } }], + ['a top-level secret', { secret: 'abc' }], + ['a token', { bundle: { windows: { token: 'abc' } } }], + ['a private key', { bundle: { macos: { privateKey: '---' } } }], + ['an oddly-cased name', { bundle: { macos: { PASSWORD: 'x' } } }], + ])('%s is refused, and its value is not printed', (_name, config) => { + const root = createProject({ files: { 'desktop/desktop.json': config } }); + let message = ''; + try { + loadDesktopConfig(root, 'macos'); + } catch (error) { + message = error.message; + } + expect(message).toMatch(/must not contain signing material/); + // Printing a leaked secret into a build log is the same mistake twice. + expect(message).not.toMatch(/hunter2|abc|---/u); + }); + + test('a key that merely resembles one is allowed', () => { + // `InfoPlist` contains "Plist", not a secret word; the check must not be so + // eager that it refuses the documented `infoPlist` setting. + const root = createProject({ + files: { + 'desktop/desktop.json': { bundle: { macos: { infoPlist: {} } } }, + }, + }); + expect(() => loadDesktopConfig(root, 'macos')).not.toThrow(); + }); +}); + +describe('the app.json migration', () => { + test('a non-empty app.json desktop key is refused with the migration', () => { + const root = createProject({ + appJson: { name: 'MyApp', desktop: { appId: 'com.example.app' } }, + }); + expect(() => loadDesktopConfig(root, 'macos')).toThrow( + 'move `app.json` desktop.* into `desktop/desktop.json` (see the README)', + ); + }); + + test('an empty app.json desktop key is tolerated', () => { + const root = createProject({ + appJson: { name: 'MyApp', desktop: {} }, + }); + expect(() => loadDesktopConfig(root, 'macos')).not.toThrow(); + }); + + test('no app.json desktop key at all is fine', () => { + const root = createProject(); + expect(() => loadDesktopConfig(root, 'macos')).not.toThrow(); + }); +}); + +describe('targetsFor', () => { + const config = targets => ({ bundle: { targets } }); + + test("'all' expands to the current OS's alpha formats", () => { + expect(targetsFor(config('all'), 'macos')).toEqual(['app', 'dmg']); + expect(targetsFor(config('all'), 'windows')).toEqual(['nsis']); + expect(targetsFor(config('all'), 'linux')).toEqual(['deb', 'appimage']); + }); + + test('an explicit list is filtered to what the OS can build', () => { + expect(targetsFor(config(['app']), 'macos')).toEqual(['app']); + }); + + test('an unknown format names every allowed one', () => { + expect(() => targetsFor(config(['msi']), 'macos')).toThrow( + /unknown bundle target\(s\) msi; expected one of app, dmg, nsis, deb, appimage or 'all'/, + ); + }); +}); diff --git a/vendor/react-native-gpui/packages/cli/src/package-desktop.js b/vendor/react-native-gpui/packages/cli/src/package-desktop.js new file mode 100644 index 0000000000..a244260a7a --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/package-desktop.js @@ -0,0 +1,718 @@ +/** + * The libraries and files a packaged app has to ship. + * + * A packaged app is copied to a machine that has no build tree, so every shared + * library the executable links at run time has to travel with it: the engine, + * the prebuilt runtime and anything a module built as a shared library. The list + * is read from what autolink recorded and from the build output, never from a + * hard-coded table, so a module that adds a library is shipped without anyone + * updating this file. + */ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +// The engine's `runtimeFiles` is a CommonJS module inside the engine package, so +// it is loaded with a require rooted at this file rather than imported. +const require = createRequire(import.meta.url); + +/** Where an app's autolink manifest lives. */ +export function autolinkManifestPath(projectRoot) { + return path.join(projectRoot, 'desktop', 'autolink', 'autolink.json'); +} + +/** The shared-library extension for an OS, or `undefined` for another OS. */ +export function libraryExtension(os) { + switch (os) { + case 'macos': + return '.dylib'; + case 'windows': + return '.dll'; + case 'linux': + return '.so'; + default: + return undefined; + } +} + +/** + * Whether a file name is a shared library a packaged app must ship. + * + * Versioned Linux libraries (`libfoo.so.1`) count: the loader asks for the + * versioned name, and shipping only the `libfoo.so` symlink ships nothing. + */ +function isSharedLibrary(name, extension) { + if (extension === undefined) return false; + if (name.endsWith(extension)) return true; + return extension === '.so' && /\.so(\.\d+)+$/u.test(name); +} + +/** The files in `directory` that are shared libraries, sorted. */ +function sharedLibrariesIn(directory, extension) { + let entries; + try { + entries = fs.readdirSync(directory, { withFileTypes: true }); + } catch { + return []; + } + return entries + .filter(entry => entry.isFile() && isSharedLibrary(entry.name, extension)) + .map(entry => path.join(directory, entry.name)) + .sort(); +} + +/** + * Every runtime library a packaged app must carry. + * + * `os` and `arch` are the ones being packaged — Node's `process.platform` and + * `process.arch` spellings — so this is testable without packaging anything. + * + * Returns absolute paths, de-duplicated and sorted. A missing autolink manifest + * is an empty list rather than an error: an app with no autolinked modules has + * nothing extra to ship. + */ +export function runtimeLibraries( + projectRoot, + { os, arch, release = true } = {}, +) { + const platform = os ?? process.platform; + const architecture = arch ?? process.arch; + const extension = libraryExtension( + platform === 'darwin' + ? 'macos' + : platform === 'win32' + ? 'windows' + : 'linux', + ); + const libraries = new Set(); + + const manifest = readManifest(autolinkManifestPath(projectRoot)); + if (manifest !== undefined) { + // The engine names its own files, because only it knows what its platform + // build produces (a dylib, a DLL, and on some builds extra archives). + for (const engine of manifest.jsEngines ?? []) { + if (typeof engine.runtimeFiles !== 'string' || !engine.runtimeFiles) { + continue; + } + let files; + try { + const resolve = require(engine.runtimeFiles); + files = resolve(platform, architecture) ?? []; + } catch { + continue; + } + for (const file of files) { + if (isSharedLibrary(path.basename(file), extension)) { + libraries.add(path.resolve(file)); + } + } + } + } + + // Libraries the build produced. The runtime's own dependencies land beside + // the crate that built them, so every candidate directory is scanned rather + // than a single assumed one. + for (const directory of buildLibraryDirectories(projectRoot, { release })) { + for (const file of sharedLibrariesIn(directory, extension)) { + libraries.add(file); + } + } + + // Finally, and most reliably, ask the executable what it links. A library can + // live outside both the profile directory and a crate's `out` — the C++ + // bridge's `libjsi` is built by CMake under its own tree — and only the + // binary's own dependencies say what it needs at run time. + for (const file of linkedSharedLibraries(projectRoot, { + release, + extension, + })) { + libraries.add(file); + } + + // One file per name. Two paths can supply the same library — the prebuilt cache + // and the CMake tree both hold `hermesvm` — and the packaging configs map each + // entry to a destination by name, so a second entry would silently target the + // same file twice. The first path wins, which is the recorded build order. + const byName = new Map(); + for (const library of libraries) { + const name = path.basename(library); + if (!byName.has(name)) byName.set(name, library); + } + return [...byName.values()].sort(); +} + +/** + * The shared libraries an executable links that are not system libraries. + * + * `otool -L` on macOS, `readelf -d` on Linux. A system library resolves from the + * OS and must not be bundled; anything resolved through a `@rpath` entry, or + * living under the build, is ours to ship. + */ +export function linkedSharedLibraries(projectRoot, { release, extension }) { + const binary = path.join( + targetProfileDir( + path.join(projectRoot, 'desktop'), + release ? 'release' : 'debug', + ), + executableName(projectRoot), + ); + if (!fs.existsSync(binary)) return []; + + let output; + try { + if (process.platform === 'darwin') { + output = execFileSync('otool', ['-L', binary], { encoding: 'utf8' }); + } else if (process.platform === 'linux') { + output = execFileSync('readelf', ['-d', binary], { encoding: 'utf8' }); + } else { + return []; + } + } catch { + return []; + } + + const found = new Set(); + for (const reference of parseSharedLibraryReferences( + output, + process.platform, + )) { + const resolved = resolveLinkedLibrary(reference, { + binary, + projectRoot, + }); + if (resolved === undefined) continue; + if (isSharedLibrary(path.basename(resolved), extension)) + found.add(resolved); + } + return [...found]; +} + +/** + * The library references in `otool -L` or `readelf -d` output. + * + * Pure, and separate from running the tool, because the two formats differ in + * the way that matters most: a Mach-O dependency names a path (`@rpath/foo`, + * `/abs/foo`), while an ELF `NEEDED` entry is a bare soname (`libjsi.so`) that + * says nothing about where the file came from. That difference is invisible to + * anyone developing on macOS, so it is tested against recorded output. + */ +export function parseSharedLibraryReferences(output, platform) { + const references = []; + for (const line of output.split('\n')) { + // `otool -L`: an indented path, then the compatibility version in parens. + // The first line is the binary's own path and must be ignored. + const machO = platform === 'darwin' ? line.match(/^\t(\S+)\s+\(/u) : null; + // `readelf -d`: `0x… (NEEDED) Shared library: [libjsi.so]`. + const elf = line.match(/NEEDED\)\s+Shared library: \[(\S+)\]/u); + const match = machO ?? elf; + if (match === null) continue; + if (!references.includes(match[1])) references.push(match[1]); + } + return references; +} + +/** + * `resolveLinkedLibrary`, exposed for tests. + * + * The bare-soname case only happens on Linux, where these tests cannot run, so + * the lookup has to be exercised from macOS against a synthesised build tree. + */ +export function resolveLinkedLibraryForTest(reference, projectRoot) { + return resolveLinkedLibrary(reference, { + binary: path.join(projectRoot, 'desktop', 'target', 'release', 'my-app'), + projectRoot, + }); +} + +/** + * Where a `@rpath`/`$ORIGIN` reference actually points, or `None` for a system + * library. + * + * The executable's own rpaths are what the loader uses, so they are what this + * walks. A reference into `/usr/lib` is the OS's to provide. + */ +function resolveLinkedLibrary(reference, { binary, projectRoot }) { + if (reference.startsWith('@rpath/')) { + const name = reference.slice('@rpath/'.length); + for (const rpath of rpathsOf(binary)) { + const candidate = path.join(rpath, name); + if (fs.existsSync(candidate)) return candidate; + } + // A packaging build carries only relative rpaths, so the absolute library + // directories are not in the file to be read back out. The directories the + // build recorded are, and they name the config it actually produced. + for (const directory of releaseLibraryDirectories(projectRoot)) { + const candidate = path.join(directory, name); + if (fs.existsSync(candidate)) return candidate; + } + return undefined; + } + if (reference.startsWith('$ORIGIN')) { + const candidate = path.join( + path.dirname(binary), + reference.slice('$ORIGIN/'.length), + ); + return fs.existsSync(candidate) ? candidate : undefined; + } + if (!path.isAbsolute(reference)) { + // ELF `NEEDED` entries are bare sonames (`libjsi.so`), not paths, so a Linux + // binary says nothing about where its libraries came from. Look for the name + // in the directories the build produced; a system library will not be there + // and is correctly skipped. + for (const directory of [ + // What the build recorded first — it names the prebuilt cache and the + // engine's own directory — then the release config's tree, then Cargo's + // release output. + ...releaseLibraryDirectories(projectRoot), + ...buildLibraryDirectories(projectRoot, { release: true }), + ]) { + const candidate = path.join(directory, reference); + if (fs.existsSync(candidate)) return candidate; + } + return undefined; + } + // A library under the repository belongs to the build; one under /usr or + // /System belongs to the OS. + if (reference.startsWith('/usr/') || reference.startsWith('/System/')) { + return undefined; + } + return reference.startsWith(projectRoot) ? reference : undefined; +} + +/** The rpaths a Mach-O binary carries, absolute entries only. */ +function rpathsOf(binary) { + let output; + try { + output = execFileSync('otool', ['-l', binary], { encoding: 'utf8' }); + } catch { + return []; + } + const out = []; + for (const line of output.split('\n')) { + const match = line.match(/^\s+path (\S+) \(offset/u); + if (match !== null && path.isAbsolute(match[1])) out.push(match[1]); + } + return out; +} + +/** + * The library directories the build itself recorded. + * + * `rn-gpui-sys` generates `rpath_dirs-.txt` while configuring CMake, and + * that list is what ends up in the executable's rpath. It is the only accurate + * answer: it names the *prebuilt* runtime's cache directory when the app used a + * prebuilt, and the Hermes binary directory, neither of which is under the build + * tree. Reading it is why a library cannot be missed by guessing at layouts. + */ +/// Where the CMake tree for this build lives. +/// +/// It sits beside the Cargo profile directory, so it moves with +/// `CARGO_TARGET_DIR`, which CI sets. +function cmakeRoot(projectRoot) { + return path.join( + path.dirname( + targetProfileDir(path.join(projectRoot, 'desktop'), 'release'), + ), + 'rn-gpui-cmake', + ); +} + +/** + * The release config directories under one triple. + * + * Only a release config may be searched: a `Debug` build's libraries are a + * different build, possibly against the other runtime, and a package that ships + * one is broken in a way no clean CI run can see — a fresh runner has only the + * single config it just built, while a developer's tree accumulates them all. + */ +function releaseConfigRoots(root) { + const found = []; + let triples; + try { + triples = fs.readdirSync(root, { withFileTypes: true }); + } catch { + return found; + } + for (const triple of triples) { + if (!triple.isDirectory()) continue; + const tripleDir = path.join(root, triple.name); + let configs; + try { + configs = fs.readdirSync(tripleDir, { withFileTypes: true }); + } catch { + continue; + } + for (const config of configs) { + if (config.isDirectory() && RELEASE_CONFIG.test(config.name)) { + found.push(path.join(tripleDir, config.name)); + } + } + } + // A prebuilt config first: it records the cache the package is actually + // linked against, so its answer outranks a source config's. + const rank = directory => (directory.endsWith('-prebuilt') ? 0 : 1); + return found.sort((a, b) => rank(a) - rank(b)); +} + +/** + * Every directory a release build may have put a shared library in. + * + * The recorded rpaths come first because they are the build's own answer, then + * the current release config's tree. Anything outside a release config is + * excluded on purpose — see `releaseConfigRoots`. + */ +export function releaseLibraryDirectories(projectRoot) { + return [ + ...recordedLibraryDirectories(projectRoot), + ...cmakeLibraryDirectories(projectRoot), + ]; +} + +export function recordedLibraryDirectories(projectRoot) { + const root = cmakeRoot(projectRoot); + const directories = []; + const candidates = releaseConfigRoots(root).flatMap(config => + walkDirectories(config), + ); + for (const file of candidates) { + let entries; + try { + entries = fs.readdirSync(file, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + if (!entry.isFile() || !/^rpath_dirs-.*\.txt$/u.test(entry.name)) + continue; + let text; + try { + text = fs.readFileSync(path.join(file, entry.name), 'utf8'); + } catch { + continue; + } + for (const line of text.split('\n')) { + const directory = line.trim(); + if (directory && !directories.includes(directory)) { + directories.push(directory); + } + } + } + } + return directories; +} + +/** + * Directories under the C++ build tree that hold its shared libraries. + * + * `libjsi` is built by CMake, not Cargo, so it is in neither the profile + * directory nor a crate's `out`: it sits under + * `/build/target/rn-gpui-cmake///build/...`. + */ +function cmakeLibraryDirectories(projectRoot) { + const found = []; + for (const configRoot of releaseConfigRoots(cmakeRoot(projectRoot))) { + // The libraries are at different depths per dependency, so walk. + for (const entry of walkDirectories(configRoot)) found.push(entry); + } + return found; +} + +/// A CMake config directory that produces a release binary. +/// +/// `Release-prebuilt` is what a prebuilt build configures; `RelWithDebInfo` is +/// what an optimised build gets when debug info is kept. `Debug*` is excluded. +const RELEASE_CONFIG = /^(?:Release|RelWithDebInfo)(?:-prebuilt)?$/u; + +/** Every directory at or below `root`, breadth-first, bounded in depth. */ +function walkDirectories(root, depth = 0) { + if (depth > 5) return []; + const out = [root]; + let entries; + try { + entries = fs.readdirSync(root, { withFileTypes: true }); + } catch { + return out; + } + for (const entry of entries) { + if (entry.isDirectory()) + out.push(...walkDirectories(path.join(root, entry.name), depth + 1)); + } + return out; +} + +/** The executable's file name, from the desktop crate's Cargo.toml. */ +function executableName(projectRoot) { + try { + const manifest = fs.readFileSync( + path.join(projectRoot, 'desktop', 'Cargo.toml'), + 'utf8', + ); + return manifest.match(/^name\s*=\s*"([^"]+)"/mu)?.[1] ?? ''; + } catch { + return ''; + } +} + +function readManifest(file) { + try { + return JSON.parse(fs.readFileSync(file, 'utf8')); + } catch { + return undefined; + } +} + +/** + * Directories the build may have put shared libraries in. + * + * Both the crate's own target directory and the one Cargo was pointed at + * (`CARGO_TARGET_DIR`), because the CLI sets the latter for reproducible paths. + */ +export function buildLibraryDirectories(projectRoot, { release = true } = {}) { + const profile = release ? 'release' : 'debug'; + const crateDir = path.join(projectRoot, 'desktop'); + // `targetProfileDir` already applies `CARGO_TARGET_DIR` and the crate's own + // `target-dir`, so the other roots are only fallbacks for a library that + // landed somewhere less predictable. + const roots = [ + path.dirname(targetProfileDir(crateDir, profile)), + path.join(crateDir, 'target'), + path.join(projectRoot, 'target'), + ].filter(Boolean); + const directories = new Set(); + for (const root of roots) { + directories.add(path.join(root, profile)); + // The prebuilt runtime's shared library is placed under the sys crate's + // build output, which is keyed by a hash that is not predictable. + const buildRoot = path.join(root, profile, 'build'); + let entries; + try { + entries = fs.readdirSync(buildRoot, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + if (!entry.isDirectory()) continue; + directories.add(path.join(buildRoot, entry.name, 'out')); + } + } + return [...directories]; +} + +/// The cargo-packager version this CLI drives and installs. +/// +/// Pinned so a config written for one version is not silently handed to another: +/// the translator emits this version's schema, and an installed copy that differs +/// is replaced. +export const CARGO_PACKAGER_VERSION = '0.11.8'; + +/** + * Signing variables cargo-packager reads from the environment itself. + * + * Nothing here is ever written into a config file: the packager skips these + * fields when parsing JSON precisely so a secret cannot be committed, and this + * CLI passes its own environment through unchanged so setting one in the shell + * or in CI is all a user does. They are listed so a test can assert their values + * never reach the generated config. + */ +export const SIGNING_ENVIRONMENT = [ + 'APPLE_CERTIFICATE', + 'APPLE_CERTIFICATE_PASSWORD', + 'APPLE_SIGNING_IDENTITY', + 'APPLE_PROVIDER_SHORT_NAME', + 'APPLE_TEAM_ID', + 'APPLE_ID', + 'APPLE_PASSWORD', + 'APPLE_API_KEY', + 'APPLE_API_ISSUER', + 'APPLE_API_KEY_PATH', + 'WINDOWS_CERTIFICATE_THUMBPRINT', + 'WINDOWS_TIMESTAMP_SERVER_URL', + 'WINDOWS_TSP', +]; + +/** cargo-packager's name for each of this phase's formats. */ +const PACKAGER_FORMAT = { + app: 'app', + dmg: 'dmg', + nsis: 'nsis', + deb: 'deb', + appimage: 'appimage', +}; + +/** + * Where Cargo put the executable for a profile. + * + * The desktop crate's `.cargo/config.toml` redirects `target-dir` to + * `build/target`, so `target/release` does not exist. Reading the setting is the + * only way to be right for both layouts. + */ +export function targetProfileDir(crateDir, profile) { + // `CARGO_TARGET_DIR` overrides everything, and CI sets it so the build cache + // survives a deleted app folder. + if (process.env.CARGO_TARGET_DIR) { + return path.join(path.resolve(process.env.CARGO_TARGET_DIR), profile); + } + const compiledIn = path.join(crateDir, 'target', profile); + let config; + try { + config = fs.readFileSync( + path.join(crateDir, '.cargo', 'config.toml'), + 'utf8', + ); + } catch { + return compiledIn; + } + const match = config.match(/^\s*target-dir\s*=\s*"([^"]+)"/mu); + if (!match) return compiledIn; + // Cargo resolves a relative `target-dir` from the directory holding `.cargo/`, + // which is the crate, and it may climb out of the crate (`../../../target`). + return path.resolve(crateDir, match[1], profile); +} + +/** + * Translates `desktop.json` into cargo-packager's config. + * + * cargo-packager is an implementation detail: users never write its JSON, and + * replacing it would only change this function. Pure, so the whole translation + * is unit-testable without packaging anything. + * + * `build` carries what the caller observed — the crate directory, the + * executable's name, the directories holding the bundle and assets, the icon + * files and the libraries to ship — because none of that is in the config file. + */ +export function toPackagerConfig(config, build) { + const { + crateDir, + binaryName, + bundleDir, + assetsDir, + icons = [], + libraries = [], + formats, + outDir, + os, + rpathDir, + } = build; + + // cargo-packager resolves paths relative to the crate directory, and its + // schema uses forward slashes on every platform. + const relative = absolute => + path.relative(crateDir, absolute).split(path.sep).join('/'); + + // The bundle and the assets travel as resources, so the release build's + // `resource_dir` finds them beside the executable. The bundle file and the + // assets directory are named separately rather than shipping the whole build + // directory, which holds the output of this very run. + const resources = []; + if (bundleDir !== undefined) { + // One file, one name. `package-desktop` stages it; there is no candidate list + // to get wrong, which is how a plain-JS bundle was shipped while the bytecode + // sat unused beside it. + const bundle = path.join(bundleDir, 'index.bundle'); + if (!fs.existsSync(bundle)) { + throw new Error(`the staged bundle is missing: ${bundle}`); + } + resources.push(relative(bundle)); + } + if (assetsDir !== undefined && fs.existsSync(assetsDir)) { + resources.push(relative(assetsDir)); + } + // On Windows the runtime DLLs are already staged beside the executable by the + // app's build.rs, and the loader looks there — but NSIS installs only the + // binary and the resources, so "beside the executable" has to be expressed as + // resources or the DLLs are left behind. Elsewhere they are handled per format + // below (`Frameworks`, `lib/`). + if (os === 'windows') { + for (const library of libraries) resources.push(relative(library)); + } + for (const extra of config.bundle.resources ?? []) { + // `bundle.resources` is written relative to `desktop/`, which is the crate. + resources.push(relative(path.resolve(crateDir, extra))); + } + + const config_ = { + $schema: + 'https://raw.githubusercontent.com/crabnebula-dev/cargo-packager/dev/crates/packager/schema.json', + name: binaryName, + productName: config.productName, + version: config.version, + identifier: config.identifier, + // `binariesDir` is where the executables are; `outDir` is where the + // packages go. Without the first, cargo-packager looks for the binary + // inside the output directory. + binariesDir: targetProfileDir(crateDir, 'release'), + binaries: [{ path: binaryName, main: true }], + outDir: outDir ?? path.join(crateDir, 'build', 'dist'), + resources, + }; + if (formats !== undefined) { + config_.formats = formats.map(format => PACKAGER_FORMAT[format]); + } + if (icons.length > 0) config_.icons = icons; + if (config.bundle.copyright !== undefined) { + config_.copyright = config.bundle.copyright; + } + if (config.bundle.category !== undefined) { + config_.category = config.bundle.category; + } + + const macos = { + minimumSystemVersion: config.bundle.macos.minimumSystemVersion, + }; + if (config.bundle.macos.entitlements !== null) { + macos.entitlements = relative( + path.resolve(process.cwd(), config.bundle.macos.entitlements), + ); + } + if (libraries.length > 0) { + // These are *source* paths, and absolute: cargo-packager copies each one + // into Contents/Frameworks, which is where the release build's + // `@executable_path/../Frameworks` rpath looks. They are absolute because a + // library can live outside the crate — the engine's come from its own + // package — and cargo-packager opens the string as given rather than + // resolving it against the config file. + macos.frameworks = libraries; + } + config_.macos = macos; + + if (config.bundle.windows.nsis !== undefined) { + config_.nsis = { installMode: config.bundle.windows.nsis.installMode }; + } + + // Linux keeps its libraries under `lib/`, which is where the release + // build's `$ORIGIN/../lib/` rpath looks. Both Linux formats map a + // source path to a destination inside the package, so the library goes to + // that directory rather than wherever resources land by default. + if (libraries.length > 0) { + // The two Linux formats need different directories, because they find + // libraries differently: + // - the deb installs to `usr/lib/`, which is where the release build's + // `$ORIGIN/../lib/` rpath looks; + // - the AppImage's `AppRun` puts `$APPDIR/usr/lib` on the library path, and + // `linuxdeploy` resolves the executable's dependencies from there before + // the bundle is assembled. A library one directory deeper is reported as + // `Could not find dependency`, which is how this was found. + const debFiles = {}; + const appimageFiles = {}; + for (const library of libraries) { + const name = path.basename(library); + // `rpathDir` is the directory the release build's rpath actually names, + // which is the app name from `app.json` — not the crate name and not + // `productName`. Using anything else installs the libraries one directory + // away from where the binary looks, and it fails to start. + debFiles[library] = `/usr/lib/${rpathDir}/${name}`; + appimageFiles[library] = `/usr/lib/${name}`; + } + config_.deb = { ...(config_.deb ?? {}), files: debFiles }; + config_.appimage = { files: appimageFiles }; + } + if (config.bundle.linux.deb.depends.length > 0) { + config_.deb = { + ...(config_.deb ?? {}), + depends: config.bundle.linux.deb.depends, + }; + } + + return config_; +} diff --git a/vendor/react-native-gpui/packages/cli/src/package-desktop.test.js b/vendor/react-native-gpui/packages/cli/src/package-desktop.test.js new file mode 100644 index 0000000000..1b9d0ee19e --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/package-desktop.test.js @@ -0,0 +1,687 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, test } from 'vitest'; +import { + buildLibraryDirectories, + CARGO_PACKAGER_VERSION, + SIGNING_ENVIRONMENT, + libraryExtension, + parseSharedLibraryReferences, + recordedLibraryDirectories, + releaseLibraryDirectories, + resolveLinkedLibraryForTest, + runtimeLibraries, + targetProfileDir, + toPackagerConfig, +} from './package-desktop.js'; + +const roots = []; + +afterEach(() => { + while (roots.length > 0) { + fs.rmSync(roots.pop(), { recursive: true, force: true }); + } +}); + +/** A project with an autolink manifest and whatever files are asked for. */ +function createProject({ engines = [], files = {} } = {}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-gpui-package-')); + roots.push(root); + write(root, 'package.json', { name: 'my-app' }); + if (engines.length > 0) { + write(root, 'desktop/autolink/autolink.json', { jsEngines: engines }); + } + for (const [relative, contents] of Object.entries(files)) { + write(root, relative, contents); + } + return root; +} + +function write(root, relative, contents) { + const target = path.join(root, relative); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync( + target, + typeof contents === 'string' + ? contents + : `${JSON.stringify(contents, null, 2)}\n`, + ); +} + +describe('libraryExtension', () => { + test('each OS has its own shared-library extension', () => { + expect(libraryExtension('macos')).toBe('.dylib'); + expect(libraryExtension('windows')).toBe('.dll'); + expect(libraryExtension('linux')).toBe('.so'); + }); +}); + +describe('runtimeLibraries', () => { + test('an engine that names its own files is read from its module', () => { + const root = createProject(); + const engineDir = path.join(root, 'engine'); + fs.mkdirSync(engineDir, { recursive: true }); + const library = path.join(engineDir, 'libhermesvm.dylib'); + fs.writeFileSync(library, 'x'); + fs.writeFileSync( + path.join(engineDir, 'runtime-files.js'), + `const path = require('node:path'); + module.exports = (platform) => + platform === 'darwin' ? [path.join(__dirname, 'libhermesvm.dylib')] : []; + `, + ); + write(root, 'desktop/autolink/autolink.json', { + jsEngines: [ + { + name: 'hermes', + runtimeFiles: path.join(engineDir, 'runtime-files.js'), + }, + ], + }); + + // The engine resolves its own directory, which on macOS reaches the same + // file through the `/var` -> `/private/var` symlink. + const libs = runtimeLibraries(root, { os: 'darwin', arch: 'arm64' }); + expect(libs.map(file => fs.realpathSync(file))).toContain( + fs.realpathSync(library), + ); + }); + + test('a library the engine does not name for this platform is skipped', () => { + const root = createProject(); + const engineDir = path.join(root, 'engine'); + fs.mkdirSync(engineDir, { recursive: true }); + fs.writeFileSync(path.join(engineDir, 'libhermesvm.dylib'), 'x'); + fs.writeFileSync( + path.join(engineDir, 'runtime-files.js'), + `const path = require('node:path'); + module.exports = (platform) => + platform === 'darwin' ? [path.join(__dirname, 'libhermesvm.dylib')] : []; + `, + ); + write(root, 'desktop/autolink/autolink.json', { + jsEngines: [ + { + name: 'hermes', + runtimeFiles: path.join(engineDir, 'runtime-files.js'), + }, + ], + }); + + expect(runtimeLibraries(root, { os: 'win32', arch: 'x64' })).toEqual([]); + }); + + test('shared libraries the build produced are found beside the target', () => { + const root = createProject(); + write(root, 'desktop/target/release/libruntime.dylib', 'x'); + write(root, 'desktop/target/release/notes.txt', 'not a library'); + const libs = runtimeLibraries(root, { os: 'darwin', arch: 'arm64' }); + expect(libs.map(file => path.basename(file))).toEqual(['libruntime.dylib']); + }); + + test('the sys crate build output is scanned, keyed by its hash', () => { + const root = createProject(); + write( + root, + 'desktop/target/release/build/rn-gpui-sys-abc123/out/libjsi.dylib', + 'x', + ); + const libs = runtimeLibraries(root, { os: 'darwin', arch: 'arm64' }); + expect(libs.map(file => path.basename(file))).toEqual(['libjsi.dylib']); + }); + + test('a versioned Linux library counts as a library', () => { + const root = createProject(); + write(root, 'desktop/target/release/libfoo.so.1.2.3', 'x'); + write(root, 'desktop/target/release/libfoo.so', 'x'); + const libs = runtimeLibraries(root, { os: 'linux', arch: 'x64' }); + expect(libs.map(file => path.basename(file)).sort()).toEqual([ + 'libfoo.so', + 'libfoo.so.1.2.3', + ]); + }); + + test('a library for another OS is not shipped', () => { + const root = createProject(); + write(root, 'desktop/target/release/libfoo.dylib', 'x'); + expect(runtimeLibraries(root, { os: 'win32', arch: 'x64' })).toEqual([]); + }); + + test('a debug build reads the debug profile', () => { + const root = createProject(); + write(root, 'desktop/target/debug/libruntime.dylib', 'x'); + write(root, 'desktop/target/release/librelease.dylib', 'x'); + expect( + runtimeLibraries(root, { + os: 'darwin', + arch: 'arm64', + release: false, + }).map(file => path.basename(file)), + ).toEqual(['libruntime.dylib']); + }); + + test('a project with no autolink manifest ships no engine files', () => { + const root = createProject(); + expect(runtimeLibraries(root, { os: 'darwin', arch: 'arm64' })).toEqual([]); + }); + + test('the same library named twice is shipped once', () => { + const root = createProject(); + write(root, 'desktop/target/release/libfoo.dylib', 'x'); + const libs = runtimeLibraries(root, { os: 'darwin', arch: 'arm64' }); + expect(new Set(libs).size).toBe(libs.length); + }); +}); + +describe('buildLibraryDirectories', () => { + test('the release and debug profiles are distinct', () => { + const root = createProject(); + expect(buildLibraryDirectories(root, { release: true })[0]).toContain( + path.join('desktop', 'target', 'release'), + ); + expect(buildLibraryDirectories(root, { release: false })[0]).toContain( + path.join('desktop', 'target', 'debug'), + ); + }); +}); + +// The translator is the whole reason cargo-packager is an implementation +// detail: these hold what it emits for one config, so replacing the packager +// only changes one function and one set of expectations. +test('every package ships an icon', () => { + // cargo-packager's AppImage step refuses to build without a square icon, so + // the CLI always supplies one: the app's, or its own placeholder. An app that + // never set `icon` still packages every format. + const withIcon = icons => + toPackagerConfig( + { + productName: 'A', + identifier: 'com.a.b', + version: '1.0.0', + bundle: { + resources: [], + macos: { minimumSystemVersion: '13.0', entitlements: null }, + windows: {}, + linux: { deb: { depends: [] }, appimage: {} }, + }, + }, + { crateDir: '/p/desktop', binaryName: 'a', icons }, + ); + + expect(withIcon(['build/icons/icon.png']).icons).toEqual([ + 'build/icons/icon.png', + ]); + expect(withIcon(['assets/icon.png']).icons).toEqual(['assets/icon.png']); +}); + +test('the packager version is pinned to one release', () => { + // The translator emits this version's schema, and `ensureCargoPackager` + // installs it, so the two cannot drift apart. + expect(CARGO_PACKAGER_VERSION).toMatch(/^\d+\.\d+\.\d+$/u); +}); + +describe('toPackagerConfig', () => { + const desktopConfig = overrides => { + const { bundle: bundleOverrides, ...rest } = overrides ?? {}; + return { + productName: 'My App', + identifier: 'com.example.myapp', + version: '1.2.3', + window: { width: 1024, height: 768 }, + devMenu: true, + ...rest, + bundle: { + targets: 'all', + resources: [], + macos: { + minimumSystemVersion: '13.0', + entitlements: null, + infoPlist: {}, + }, + windows: { nsis: { installMode: 'currentUser' } }, + linux: { deb: { depends: [] }, appimage: {} }, + ...bundleOverrides, + }, + }; + }; + const build = overrides => ({ + crateDir: '/project/desktop', + binaryName: 'my-app', + formats: ['app', 'dmg'], + ...overrides, + }); + + test('a minimal app gives the fields cargo-packager needs', () => { + const config = toPackagerConfig(desktopConfig(), build()); + expect(config.productName).toBe('My App'); + expect(config.identifier).toBe('com.example.myapp'); + expect(config.version).toBe('1.2.3'); + expect(config.binariesDir).toBe('/project/desktop/target/release'); + expect(config.binaries).toEqual([{ path: 'my-app', main: true }]); + expect(config.formats).toEqual(['app', 'dmg']); + expect(config.outDir).toBe('/project/desktop/build/dist'); + }); + + test('the bundle file and the assets directory ship as resources', () => { + // A directory containing this run's output cannot be shipped, so the two + // are named. The asset directory has to exist to be listed. + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-gpui-res-')); + roots.push(root); + const crateDir = path.join(root, 'desktop'); + fs.mkdirSync(path.join(crateDir, 'build', 'assets'), { recursive: true }); + fs.writeFileSync(path.join(crateDir, 'build', 'index.bundle'), 'x'); + const config = toPackagerConfig( + desktopConfig(), + build({ + crateDir, + bundleDir: path.join(crateDir, 'build'), + assetsDir: path.join(crateDir, 'build', 'assets'), + }), + ); + expect(config.resources).toEqual(['build/index.bundle', 'build/assets']); + }); + + test('a missing staged bundle is an error, not a silent omission', () => { + // A package without its bundle starts and then dies on a machine with no + // build tree, so the failure belongs here rather than at first launch. + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-gpui-res-')); + roots.push(root); + const crateDir = path.join(root, 'desktop'); + fs.mkdirSync(path.join(crateDir, 'build'), { recursive: true }); + expect(() => + toPackagerConfig( + desktopConfig(), + build({ crateDir, bundleDir: path.join(crateDir, 'build') }), + ), + ).toThrow(/staged bundle is missing/u); + }); + + test('extra resources are relative to the crate', () => { + const config = toPackagerConfig( + desktopConfig({ bundle: { resources: ['extra/data.json'] } }), + build(), + ); + expect(config.resources).toEqual(['extra/data.json']); + }); + + test('runtime libraries become macOS frameworks, by source path', () => { + // cargo-packager copies each entry into Contents/Frameworks itself, so the + // entry is where the library is, not where it lands. + const config = toPackagerConfig( + desktopConfig(), + build({ libraries: ['/project/desktop/build/libjsi.dylib'] }), + ); + expect(config.macos.frameworks).toEqual([ + '/project/desktop/build/libjsi.dylib', + ]); + }); + + test('runtime libraries get the directory each Linux format searches', () => { + // The deb installs to `usr/lib/`, where the binary's + // `$ORIGIN/../lib/` rpath looks. The AppImage needs flat `usr/lib`, + // which is what AppRun puts on the library path and where linuxdeploy looks + // when resolving the executable's dependencies. + const config = toPackagerConfig( + desktopConfig(), + build({ rpathDir: 'My App', libraries: ['/build/libjsi.so'] }), + ); + // `rpathDir` is the app name the rpath names, not the crate name: the + // libraries have to land where the binary actually looks. + expect(config.deb.files).toEqual({ + '/build/libjsi.so': '/usr/lib/My App/libjsi.so', + }); + expect(config.appimage.files).toEqual({ + '/build/libjsi.so': '/usr/lib/libjsi.so', + }); + }); + + test('Windows ships the DLLs as resources', () => { + // NSIS installs the binary and the resources, not the profile directory + // beside the executable, so the DLLs have to be resources or they are left + // behind and the app fails to start. + const config = toPackagerConfig( + desktopConfig(), + build({ + os: 'windows', + crateDir: '/project/desktop', + libraries: ['/project/desktop/build/hermesvm.dll'], + }), + ); + expect(config.resources).toContain('build/hermesvm.dll'); + }); + + test('macOS and Linux do not ship libraries as resources', () => { + for (const platform of ['macos', 'linux']) { + const config = toPackagerConfig( + desktopConfig(), + build({ + os: platform, + libraries: ['/project/desktop/build/libjsi.dylib'], + }), + ); + expect(config.resources).not.toContain('build/libjsi.dylib'); + } + }); + + test('no libraries means no empty framework or files block', () => { + const config = toPackagerConfig(desktopConfig(), build()); + expect(config.macos.frameworks).toBeUndefined(); + expect(config.deb).toBeUndefined(); + expect(config.appimage).toBeUndefined(); + }); + + test('the NSIS install mode is carried over', () => { + const config = toPackagerConfig( + desktopConfig({ + bundle: { windows: { nsis: { installMode: 'perMachine' } } }, + }), + build({ formats: ['nsis'] }), + ); + expect(config.nsis).toEqual({ installMode: 'perMachine' }); + }); + + test('Debian dependencies are carried over', () => { + const config = toPackagerConfig( + desktopConfig({ + bundle: { linux: { deb: { depends: ['libgtk-3-0'] } } }, + }), + build({ formats: ['deb'] }), + ); + expect(config.deb.depends).toEqual(['libgtk-3-0']); + }); + + test('a copyright and category reach the output when set', () => { + const config = toPackagerConfig( + desktopConfig({ + bundle: { copyright: '© 2026 Me', category: 'DeveloperTool' }, + }), + build(), + ); + expect(config.copyright).toBe('© 2026 Me'); + expect(config.category).toBe('DeveloperTool'); + }); + + // D6: signing material comes from the environment, so no value of it may + // appear in the generated config, whatever the caller's environment holds. + test('no signing material appears in the output', () => { + const saved = new Map(); + for (const name of SIGNING_ENVIRONMENT) { + saved.set(name, process.env[name]); + process.env[name] = `SECRET-${name}`; + } + try { + const text = JSON.stringify( + toPackagerConfig( + desktopConfig(), + build({ libraries: ['/b/lib.dylib'] }), + ), + ); + for (const name of SIGNING_ENVIRONMENT) { + expect(text).not.toContain(`SECRET-${name}`); + } + // Nor may the variable names themselves appear: a config naming a + // credential is a config someone will fill in. + for (const name of SIGNING_ENVIRONMENT) { + expect(text).not.toContain(name); + } + } finally { + for (const [name, value] of saved) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + } + }); + + test('paths use forward slashes on every platform', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-gpui-slash-')); + roots.push(root); + const crateDir = path.join(root, 'desktop'); + fs.mkdirSync(path.join(crateDir, 'build'), { recursive: true }); + fs.writeFileSync(path.join(crateDir, 'build', 'index.bundle'), 'x'); + const config = toPackagerConfig( + desktopConfig(), + build({ crateDir, bundleDir: path.join(crateDir, 'build') }), + ); + for (const entry of [ + ...config.resources, + ...config.binaries.map(binary => binary.path), + ]) { + expect(entry).not.toContain('\\'); + } + }); +}); + +describe('targetProfileDir', () => { + test('CARGO_TARGET_DIR wins over everything', () => { + // CI sets it so the Cargo cache survives a deleted app directory; a config + // that ignored it looked for the binary in a tree that does not exist. + const root = createProject(); + const saved = process.env.CARGO_TARGET_DIR; + process.env.CARGO_TARGET_DIR = '/ci/target'; + try { + expect(targetProfileDir(path.join(root, 'desktop'), 'release')).toBe( + path.join('/ci/target', 'release'), + ); + } finally { + if (saved === undefined) delete process.env.CARGO_TARGET_DIR; + else process.env.CARGO_TARGET_DIR = saved; + } + }); + + test('a crate with no cargo config uses target/', () => { + const root = createProject(); + expect(targetProfileDir(path.join(root, 'desktop'), 'release')).toBe( + path.join(root, 'desktop', 'target', 'release'), + ); + }); + + test("the crate's target-dir setting wins", () => { + // The scaffold redirects `target-dir` to `build/target` so build output does + // not sit beside the sources; `target/release` then does not exist at all. + const root = createProject({ + files: { + 'desktop/.cargo/config.toml': '[build]\ntarget-dir = "build/target"\n', + }, + }); + expect(targetProfileDir(path.join(root, 'desktop'), 'release')).toBe( + path.join(root, 'desktop', 'build', 'target', 'release'), + ); + }); + + test("the crate's target-dir may climb to a shared root target", () => { + // The tester desktop shares the repository's one `target/` by climbing out + // of its crate; resolving that from the crate is what finds the binary. + const root = createProject({ + files: { + 'desktop/.cargo/config.toml': + '[build]\ntarget-dir = "../../../target"\n', + }, + }); + expect(targetProfileDir(path.join(root, 'desktop'), 'release')).toBe( + path.resolve(root, 'desktop', '../../../target', 'release'), + ); + }); +}); + +// The Linux failure this covers was invisible from macOS for a whole round: an +// ELF `NEEDED` entry is a bare soname, so it must be resolved by searching the +// build, while a Mach-O dependency names its own path. +describe('parseSharedLibraryReferences', () => { + const readelf = ` + 0x0000000000000001 (NEEDED) Shared library: [libjsi.so] + 0x0000000000000001 (NEEDED) Shared library: [libhermesvm.so] + 0x0000000000000001 (NEEDED) Shared library: [libgtk-3.so.0] + 0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib/my-app] +`; + + test('ELF NEEDED entries come back as bare sonames', () => { + expect(parseSharedLibraryReferences(readelf, 'linux')).toEqual([ + 'libjsi.so', + 'libhermesvm.so', + 'libgtk-3.so.0', + ]); + }); + + test('a RUNPATH line is not mistaken for a library', () => { + expect( + parseSharedLibraryReferences(readelf, 'linux').some(reference => + reference.includes('ORIGIN'), + ), + ).toBe(false); + }); + + test("otool's first line is the binary itself, not a dependency", () => { + const otool = [ + '/build/app/Contents/MacOS/app:', + '\t@rpath/libjsi.dylib (compatibility version 0.0.0, current version 0.0.0)', + '\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0)', + ].join('\n'); + expect(parseSharedLibraryReferences(otool, 'darwin')).toEqual([ + '@rpath/libjsi.dylib', + '/usr/lib/libSystem.B.dylib', + ]); + }); + + test('a repeated dependency is listed once', () => { + const repeated = `${readelf}\n 0x0 (NEEDED) Shared library: [libjsi.so]`; + expect( + parseSharedLibraryReferences(repeated, 'linux').filter( + reference => reference === 'libjsi.so', + ), + ).toHaveLength(1); + }); +}); + +test('a bare soname is found in the CMake tree, and a system library is not', () => { + // The resolver cannot shell out to readelf on macOS, so the lookup it performs + // for a bare ELF name is tested directly. The layout is the scaffolded one: + // `.cargo/config.toml` redirects target-dir to `build/target`, and the C++ + // runtime's libraries are under `rn-gpui-cmake///...`. + const libjsi = + 'desktop/build/target/rn-gpui-cmake/x86_64-unknown-linux-gnu/Release/build/ReactCommon/jsi/libjsi.so'; + const root = createProject({ + files: { + 'desktop/.cargo/config.toml': '[build]\ntarget-dir = "build/target"\n', + 'desktop/Cargo.toml': '[package]\nname = "my-app"\nversion = "0.1.0"\n', + [libjsi]: 'x', + }, + }); + expect(resolveLinkedLibraryForTest('libjsi.so', root)).toBe( + path.join(root, libjsi), + ); + // A library the OS provides must not be shipped. + expect(resolveLinkedLibraryForTest('libgtk-3.so.0', root)).toBeUndefined(); +}); + +describe('recordedLibraryDirectories', () => { + test('the directories CMake recorded are read back', () => { + // The build writes `rpath_dirs-.txt` while configuring, and that list + // becomes the executable's rpath. It is the only source that names the + // prebuilt runtime's cache, which is not under the build tree at all. + const root = createProject({ + files: { + 'desktop/.cargo/config.toml': '[build]\ntarget-dir = "build/target"\n', + 'desktop/build/target/rn-gpui-cmake/x86_64-unknown-linux-gnu/Release/build/rpath_dirs-Release.txt': + '/opt/prebuilt/0.0.3/linux-x64/lib\n/opt/hermes/bin/linux-x64\n', + }, + }); + expect(recordedLibraryDirectories(root)).toEqual([ + '/opt/prebuilt/0.0.3/linux-x64/lib', + '/opt/hermes/bin/linux-x64', + ]); + }); + + test('a project with no build yet records nothing', () => { + expect(recordedLibraryDirectories(createProject())).toEqual([]); + }); +}); + +test('a prebuilt-only library is found through the recorded directories', () => { + // The case that failed on Linux: the app uses the prebuilt runtime, so + // `libjsi.so` exists only in the prebuilt cache, outside both the project and + // the build tree. The recorded rpath list is what makes it findable. + const root = createProject({ + files: { + 'desktop/.cargo/config.toml': '[build]\ntarget-dir = "build/target"\n', + 'desktop/Cargo.toml': '[package]\nname = "my-app"\nversion = "0.1.0"\n', + }, + }); + const cache = path.join(root, 'prebuilt-cache', 'linux-x64', 'lib'); + fs.mkdirSync(cache, { recursive: true }); + const library = path.join(cache, 'libjsi.so'); + fs.writeFileSync(library, 'x'); + write( + root, + 'desktop/build/target/rn-gpui-cmake/x86_64-unknown-linux-gnu/Release/build/rpath_dirs-Release.txt', + `${cache}\n`, + ); + expect(resolveLinkedLibraryForTest('libjsi.so', root)).toBe(library); +}); + +describe('a package only ships libraries from its own release build', () => { + // A clean runner has one config, so this cannot show up on CI. A developer's + // tree accumulates Debug, Release and both -prebuilt variants; the first match + // used to win and Debug sorts first, so a release package could pick up a Debug + // library, or one built against the other runtime. + function projectWithBothConfigs() { + const root = createProject({ + files: { + 'desktop/.cargo/config.toml': '[build]\ntarget-dir = "build/target"\n', + 'desktop/Cargo.toml': '[package]\nname = "my-app"\nversion = "0.1.0"\n', + // A Debug library, and a Release one, at the same relative depth. + 'desktop/build/target/rn-gpui-cmake/x86_64-unknown-linux-gnu/Debug/build/ReactCommon/jsi/libjsi.so': + 'debug', + 'desktop/build/target/rn-gpui-cmake/x86_64-unknown-linux-gnu/Release/build/ReactCommon/jsi/libjsi.so': + 'release', + }, + }); + return root; + } + + test('the Debug copy is not found', () => { + const root = projectWithBothConfigs(); + const found = resolveLinkedLibraryForTest('libjsi.so', root); + expect(found).toBeDefined(); + expect(found).toContain('Release'); + expect(found).not.toContain('Debug'); + }); + + test('every directory offered to the resolver is a release config', () => { + const root = projectWithBothConfigs(); + for (const directory of releaseLibraryDirectories(root)) { + expect(directory).not.toMatch(/[/\\]Debug/u); + } + }); + + test('Release-prebuilt outranks Release', () => { + const root = createProject({ + files: { + 'desktop/.cargo/config.toml': '[build]\ntarget-dir = "build/target"\n', + 'desktop/build/target/rn-gpui-cmake/x86_64-unknown-linux-gnu/Release/build/rpath_dirs-Release.txt': + '/from/release\n', + 'desktop/build/target/rn-gpui-cmake/x86_64-unknown-linux-gnu/Release-prebuilt/build/rpath_dirs-Release.txt': + '/from/release-prebuilt\n', + }, + }); + expect(recordedLibraryDirectories(root)[0]).toBe('/from/release-prebuilt'); + }); +}); + +test('a library reached by two paths is shipped once', () => { + // The prebuilt cache and the CMake tree can both hold the same engine library. + // The packaging configs map each entry to a destination by file name, so a + // second entry would target the same file twice. + const root = createProject({ + files: { + 'desktop/.cargo/config.toml': '[build]\ntarget-dir = "build/target"\n', + 'desktop/Cargo.toml': '[package]\nname = "my-app"\nversion = "0.1.0"\n', + 'desktop/target/release/libfoo.dylib': 'x', + 'desktop/build/target/release/libfoo.dylib': 'x', + }, + }); + const names = runtimeLibraries(root, { os: 'darwin', arch: 'arm64' }).map( + file => path.basename(file), + ); + expect(names.filter(name => name === 'libfoo.dylib')).toHaveLength(1); +}); diff --git a/vendor/react-native-gpui/packages/cli/src/target-gc.js b/vendor/react-native-gpui/packages/cli/src/target-gc.js new file mode 100644 index 0000000000..f14a8f3d3c --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/target-gc.js @@ -0,0 +1,386 @@ +/** + * Keeps a Cargo target directory from growing without bound. + * + * Cargo never deletes anything from a target directory on its own. Every change + * that alters a unit's hash — a feature toggled, a dependency bumped, a new + * toolchain, an environment variable a build script tracks — leaves the old + * unit beside the new one, and incremental caches stay behind for crates that + * are no longer rebuilt. Over weeks of development that is tens of gigabytes. + * + * So the CLI records which units each build used — Cargo reports every one, + * fresh or rebuilt, as JSON — and before a build removes units no recorded build + * has used for a while. Access times cannot stand in for the record: Windows + * does not update them by default, and a fresh unit is never written to. + * + * A unit with no record — built by a plain `cargo` command, or by a workspace + * that shares the directory without going through this CLI — is only removed + * once a newer unit of the same crate and kind has replaced it, so a build the + * CLI never saw is not thrown away just for being old. + * + * @format + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import readline from 'node:readline'; +import { execFileSync, spawn } from 'node:child_process'; + +export const DEFAULT_MAX_AGE_DAYS = 7; +const DAY_MS = 24 * 60 * 60 * 1000; + +/** + * The target directory Cargo will build `manifest` into, or `null`. + * + * Asked of Cargo rather than worked out here: `CARGO_TARGET_DIR`, the crate's + * own `.cargo/config.toml` and any parent directory's config all decide it. + */ +export function cargoTargetDirectory(manifest, { cwd, env } = {}) { + try { + const metadata = JSON.parse( + execFileSync( + 'cargo', + [ + 'metadata', + '--no-deps', + '--format-version', + '1', + '--manifest-path', + manifest, + ], + { + cwd, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + maxBuffer: 64 * 1024 * 1024, + }, + ), + ); + return path.resolve(metadata.target_directory); + } catch { + return null; + } +} + +/** Where the record of used units lives, inside the target directory. */ +export const USAGE_FILE = '.rn-gpui-target-usage.json'; + +/** `-`, as Cargo names a unit's fingerprint and build directory. */ +const UNIT_DIR = /^(.+)-([0-9a-f]{16})$/u; +/** The hash in a file under `deps/`: `libfoo-.rlib`, `foo-.exe`. */ +const HASHED_FILE = /-([0-9a-f]{16})(?:\.|$)/u; + +/** Whether the collector runs: never in CI, and never when turned off. */ +export function targetGcEnabled(env = process.env) { + return !env.CI && env.RN_GPUI_TARGET_GC !== '0'; +} + +/** How many days an unused unit is kept, from `RN_GPUI_TARGET_GC_DAYS`. */ +export function targetGcMaxAgeDays(env = process.env) { + const days = Number(env.RN_GPUI_TARGET_GC_DAYS); + return Number.isFinite(days) && days > 0 ? days : DEFAULT_MAX_AGE_DAYS; +} + +/** + * The usage key of the unit a build output belongs to, or `null`. + * + * Keyed by the profile directory and the hash, which together name one unit: + * `debug/0123456789abcdef`. The hash is shared by the unit's fingerprint + * directory, its `build/` directory and its files in `deps/`. + */ +export function unitKey(targetDir, file) { + const parts = path.relative(targetDir, file).split(/[\\/]/u); + const at = parts.findIndex(part => + ['deps', 'build', 'examples', '.fingerprint'].includes(part), + ); + if (at <= 0 || at + 1 >= parts.length) return null; + const profile = parts.slice(0, at).join('/'); + const entry = parts[at + 1]; + const match = + parts[at] === 'build' || parts[at] === '.fingerprint' + ? UNIT_DIR.exec(entry) + : HASHED_FILE.exec(entry); + if (!match) return null; + const hash = match[match.length - 1]; + return `${profile}/${hash}`; +} + +function readUsage(targetDir) { + try { + const parsed = JSON.parse( + fs.readFileSync(path.join(targetDir, USAGE_FILE), 'utf8'), + ); + if (parsed && typeof parsed.units === 'object') return parsed; + } catch {} + return { version: 1, units: {} }; +} + +function writeUsage(targetDir, usage) { + try { + fs.writeFileSync(path.join(targetDir, USAGE_FILE), JSON.stringify(usage)); + } catch { + // The record only makes pruning more precise; a build never fails over it. + } +} + +/** Marks every unit the given build outputs belong to as used `now`. */ +export function recordUsage(targetDir, files, now = Date.now()) { + if (!fs.existsSync(targetDir)) return; + const usage = readUsage(targetDir); + for (const file of files) { + const key = unitKey(targetDir, file); + if (key) usage.units[key] = now; + } + writeUsage(targetDir, usage); +} + +/** + * The profile directories in a target directory: each one holds `.fingerprint`. + * A host build puts them at `/`, a cross build at + * `//`. + */ +function profileDirs(targetDir) { + const found = []; + const visit = (relative, depth) => { + const absolute = path.join(targetDir, relative); + if (fs.existsSync(path.join(absolute, '.fingerprint'))) { + found.push(relative); + return; + } + if (depth === 0) return; + let entries = []; + try { + entries = fs.readdirSync(absolute, { withFileTypes: true }); + } catch {} + for (const entry of entries) { + if (entry.isDirectory() && !entry.name.startsWith('.')) { + visit(path.join(relative, entry.name), depth - 1); + } + } + }; + visit('', 2); + return found.filter(Boolean); +} + +/** The newest modification time of a directory and its direct children. */ +function newestMtime(dir) { + let newest = 0; + try { + newest = fs.statSync(dir).mtimeMs; + for (const entry of fs.readdirSync(dir)) { + try { + newest = Math.max(newest, fs.statSync(path.join(dir, entry)).mtimeMs); + } catch {} + } + } catch {} + return newest; +} + +/** + * The kind of a unit, from its fingerprint: `lib-foo`, + * `build-script-build-script-build`, `run-build-script-build-script-build`. + * One package has several units of different kinds, none replacing another. + */ +function unitKind(fingerprintDir) { + try { + const json = fs + .readdirSync(fingerprintDir) + .find(name => name.endsWith('.json')); + return json ? json.slice(0, -'.json'.length) : ''; + } catch { + return ''; + } +} + +/** + * What a prune would remove: units and incremental caches, without touching + * anything. Pure apart from reading, so it is what the tests exercise. + */ +export function findStale( + targetDir, + { now = Date.now(), maxAgeDays = DEFAULT_MAX_AGE_DAYS } = {}, +) { + const maxAge = maxAgeDays * DAY_MS; + const usage = readUsage(targetDir); + const units = []; + const incremental = []; + for (const profile of profileDirs(targetDir)) { + const profileKey = profile.split(path.sep).join('/'); + const fingerprints = path.join(targetDir, profile, '.fingerprint'); + const groups = new Map(); + for (const dirName of fs.readdirSync(fingerprints)) { + const match = UNIT_DIR.exec(dirName); + if (!match) continue; + const fingerprintDir = path.join(fingerprints, dirName); + const key = `${profileKey}/${match[2]}`; + const recorded = usage.units[key]; + const unit = { + profile, + dirName, + hash: match[2], + key, + recorded, + lastUsed: recorded ?? newestMtime(fingerprintDir), + }; + const group = `${match[1]}\u0000${unitKind(fingerprintDir)}`; + if (!groups.has(group)) groups.set(group, []); + groups.get(group).push(unit); + } + for (const group of groups.values()) { + const newest = Math.max(...group.map(unit => unit.lastUsed)); + for (const unit of group) { + if (now - unit.lastUsed <= maxAge) continue; + // A recorded unit that no build has used for the whole period is + // stale. One without a record is only stale once something newer of + // the same crate and kind has taken its place. + if (unit.recorded != null || unit.lastUsed < newest) units.push(unit); + } + } + const incrementalDir = path.join(targetDir, profile, 'incremental'); + let caches = []; + try { + caches = fs.readdirSync(incrementalDir); + } catch {} + for (const cache of caches) { + const cacheDir = path.join(incrementalDir, cache); + // A session directory is written each time the crate is compiled, so a + // cache untouched for the period gives the next build nothing to reuse. + if (now - newestMtime(cacheDir) > maxAge) incremental.push(cacheDir); + } + } + return { units, incremental }; +} + +function sizeOf(target) { + let total = 0; + try { + const stat = fs.lstatSync(target); + if (!stat.isDirectory()) return stat.size; + for (const entry of fs.readdirSync(target)) { + total += sizeOf(path.join(target, entry)); + } + } catch {} + return total; +} + +function remove(target) { + const size = sizeOf(target); + try { + fs.rmSync(target, { recursive: true, force: true }); + return size; + } catch { + return 0; + } +} + +/** Removes one unit: its fingerprint, its `build/` directory and its files. */ +function removeUnit(targetDir, unit) { + const profileDir = path.join(targetDir, unit.profile); + let freed = remove(path.join(profileDir, '.fingerprint', unit.dirName)); + freed += remove(path.join(profileDir, 'build', unit.dirName)); + for (const folder of ['deps', 'examples']) { + const dir = path.join(profileDir, folder); + let entries = []; + try { + entries = fs.readdirSync(dir); + } catch {} + for (const entry of entries) { + const match = HASHED_FILE.exec(entry); + if (match && match[1] === unit.hash) + freed += remove(path.join(dir, entry)); + } + } + return freed; +} + +/** + * Removes what `findStale` reports, and says how much it freed. Quiet when + * there was nothing to remove, so an ordinary build prints nothing extra. + */ +export function pruneTarget( + targetDir, + { + now = Date.now(), + maxAgeDays = DEFAULT_MAX_AGE_DAYS, + log = message => console.log(message), + } = {}, +) { + if (!fs.existsSync(targetDir)) return { units: 0, caches: 0, bytes: 0 }; + const stale = findStale(targetDir, { now, maxAgeDays }); + let bytes = 0; + for (const unit of stale.units) bytes += removeUnit(targetDir, unit); + for (const cache of stale.incremental) bytes += remove(cache); + if (stale.units.length > 0) { + const usage = readUsage(targetDir); + for (const unit of stale.units) delete usage.units[unit.key]; + writeUsage(targetDir, usage); + } + if (bytes > 0) { + log( + `Removed ${stale.units.length} build unit(s) and ` + + `${stale.incremental.length} incremental cache(s) unused for ` + + `${maxAgeDays} days from ${targetDir} ` + + `(${(bytes / 1024 ** 3).toFixed(2)} GB). ` + + 'Set RN_GPUI_TARGET_GC=0 to keep them.', + ); + } + return { + units: stale.units.length, + caches: stale.incremental.length, + bytes, + }; +} + +/** + * Runs a Cargo build and records the units it used. + * + * `json-render-diagnostics` leaves the diagnostics to Cargo, rendered on stderr + * as usual, and puts only the build messages on stdout as JSON; anything on + * stdout that is not one of them — a build script's own output — is passed + * through. Resolves when Cargo succeeds and rejects when it fails, like the + * plain invocation it replaces. + */ +export function cargoBuildRecording(args, { cwd, env, targetDir }) { + const [command, ...rest] = args; + const cargoArgs = [ + command, + '--message-format=json-render-diagnostics', + ...rest, + ]; + return new Promise((resolve, reject) => { + const child = spawn('cargo', cargoArgs, { + cwd, + env, + stdio: ['inherit', 'pipe', 'inherit'], + }); + const used = []; + const lines = readline.createInterface({ input: child.stdout }); + lines.on('line', line => { + let message = null; + if (line.startsWith('{')) { + try { + message = JSON.parse(line); + } catch {} + } + if (message == null || typeof message.reason !== 'string') { + process.stdout.write(`${line}\n`); + return; + } + if (message.reason === 'compiler-artifact') { + used.push(...(message.filenames ?? [])); + if (message.executable) used.push(message.executable); + } else if (message.reason === 'build-script-executed') { + if (message.out_dir) used.push(message.out_dir); + } + }); + child.on('error', reject); + child.on('close', code => { + if (code !== 0) { + reject(new Error(`cargo ${cargoArgs.join(' ')} failed with ${code}`)); + return; + } + if (targetDir) recordUsage(targetDir, used); + resolve(); + }); + }); +} diff --git a/vendor/react-native-gpui/packages/cli/src/target-gc.test.js b/vendor/react-native-gpui/packages/cli/src/target-gc.test.js new file mode 100644 index 0000000000..831c55433f --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/src/target-gc.test.js @@ -0,0 +1,224 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, test } from 'vitest'; +import { + USAGE_FILE, + findStale, + pruneTarget, + recordUsage, + targetGcEnabled, + targetGcMaxAgeDays, + unitKey, +} from './target-gc.js'; + +const DAY = 24 * 60 * 60 * 1000; +const NOW = Date.UTC(2026, 9, 24); +const HASH_A = '0123456789abcdef'; +const HASH_B = 'fedcba9876543210'; +const HASH_C = '00112233445566ff'; + +const roots = []; +afterEach(() => { + for (const root of roots.splice(0)) { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +function targetDir() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-gpui-target-gc-')); + roots.push(root); + return root; +} + +function write(file, contents = 'x') { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, contents); +} + +function age(target, daysOld) { + const time = new Date(NOW - daysOld * DAY); + fs.utimesSync(target, time, time); +} + +/** + * One unit as Cargo lays it out: a fingerprint whose `.json` names its kind, a + * library in `deps/`, and for a build script its `build/` directory. + */ +function unit( + target, + { name, hash, kind = `lib-${name}`, daysOld, profile = 'debug' }, +) { + const profileDir = path.join(target, profile); + const fingerprint = path.join(profileDir, '.fingerprint', `${name}-${hash}`); + write(path.join(fingerprint, kind)); + write(path.join(fingerprint, `${kind}.json`), '{}'); + const crate = name.replaceAll('-', '_'); + write(path.join(profileDir, 'deps', `lib${crate}-${hash}.rlib`), 'rlib'); + write(path.join(profileDir, 'deps', `${crate}-${hash}.d`)); + if (kind.includes('build-script')) { + write( + path.join(profileDir, 'build', `${name}-${hash}`, 'out', 'generated.rs'), + ); + } + for (const entry of fs.readdirSync(fingerprint)) { + age(path.join(fingerprint, entry), daysOld); + } + age(fingerprint, daysOld); + return { profileDir, fingerprint, crate }; +} + +describe('unitKey', () => { + test('names a unit by its profile and hash, from any of its outputs', () => { + const target = targetDir(); + expect( + unitKey( + target, + path.join(target, 'debug', 'deps', `libserde-${HASH_A}.rlib`), + ), + ).toBe(`debug/${HASH_A}`); + expect( + unitKey( + target, + path.join(target, 'debug', 'build', `rn-gpui-sys-${HASH_B}`, 'out'), + ), + ).toBe(`debug/${HASH_B}`); + // A cross build nests the profile under the target triple. + expect( + unitKey( + target, + path.join( + target, + 'x86_64-pc-windows-msvc', + 'release', + 'deps', + `foo-${HASH_C}.dll`, + ), + ), + ).toBe(`x86_64-pc-windows-msvc/release/${HASH_C}`); + }); + + test('an uplifted binary belongs to no unit', () => { + const target = targetDir(); + expect(unitKey(target, path.join(target, 'debug', 'app.exe'))).toBeNull(); + }); +}); + +describe('pruning', () => { + test('a recorded unit no build used for the period is removed, with its files', () => { + const target = targetDir(); + const old = unit(target, { name: 'serde', hash: HASH_A, daysOld: 30 }); + unit(target, { name: 'log', hash: HASH_B, daysOld: 30 }); + recordUsage( + target, + [path.join(old.profileDir, 'deps', `libserde-${HASH_A}.rlib`)], + NOW - 10 * DAY, + ); + recordUsage( + target, + [path.join(old.profileDir, 'deps', `liblog-${HASH_B}.rlib`)], + NOW - DAY, + ); + + const result = pruneTarget(target, { + now: NOW, + maxAgeDays: 7, + log: () => {}, + }); + + expect(result.units).toBe(1); + expect(fs.existsSync(old.fingerprint)).toBe(false); + expect( + fs.existsSync( + path.join(old.profileDir, 'deps', `libserde-${HASH_A}.rlib`), + ), + ).toBe(false); + expect( + fs.existsSync(path.join(old.profileDir, 'deps', `serde-${HASH_A}.d`)), + ).toBe(false); + // Used yesterday, so kept, and its record survives. + expect( + fs.existsSync(path.join(old.profileDir, 'deps', `liblog-${HASH_B}.rlib`)), + ).toBe(true); + const usage = JSON.parse( + fs.readFileSync(path.join(target, USAGE_FILE), 'utf8'), + ); + expect(Object.keys(usage.units)).toEqual([`debug/${HASH_B}`]); + }); + + test('an old unit without a record is kept while it is the only one of its kind', () => { + // Built by a plain `cargo` command the CLI never saw: old is not enough. + const target = targetDir(); + unit(target, { name: 'gpui', hash: HASH_A, daysOld: 60 }); + expect(findStale(target, { now: NOW, maxAgeDays: 7 }).units).toEqual([]); + }); + + test('an old unit without a record goes once a newer one of the same kind replaced it', () => { + const target = targetDir(); + unit(target, { name: 'rn-gpui-sys', hash: HASH_A, daysOld: 30 }); + unit(target, { name: 'rn-gpui-sys', hash: HASH_B, daysOld: 1 }); + const stale = findStale(target, { now: NOW, maxAgeDays: 7 }); + expect(stale.units.map(staleUnit => staleUnit.hash)).toEqual([HASH_A]); + }); + + test("a package's build-script units are not replaced by its library", () => { + // One package has a library, a build script and the build script's run, + // each its own unit. The library being newer replaces none of the others. + const target = targetDir(); + const name = 'rn-gpui-sys'; + unit(target, { + name, + hash: HASH_A, + daysOld: 30, + kind: 'build-script-build-script-build', + }); + unit(target, { + name, + hash: HASH_B, + daysOld: 30, + kind: 'run-build-script-build-script-build', + }); + unit(target, { name, hash: HASH_C, daysOld: 1 }); + expect(findStale(target, { now: NOW, maxAgeDays: 7 }).units).toEqual([]); + }); + + test('an incremental cache untouched for the period is removed', () => { + const target = targetDir(); + unit(target, { name: 'app', hash: HASH_A, daysOld: 1 }); + const incremental = path.join(target, 'debug', 'incremental'); + write(path.join(incremental, 'app-old', 's-session', 'query-cache.bin')); + age(path.join(incremental, 'app-old', 's-session'), 20); + age(path.join(incremental, 'app-old'), 20); + write(path.join(incremental, 'app-new', 's-session', 'query-cache.bin')); + age(path.join(incremental, 'app-new', 's-session'), 0); + age(path.join(incremental, 'app-new'), 0); + + pruneTarget(target, { now: NOW, maxAgeDays: 7, log: () => {} }); + + expect(fs.existsSync(path.join(incremental, 'app-old'))).toBe(false); + expect(fs.existsSync(path.join(incremental, 'app-new'))).toBe(true); + }); + + test('a missing target directory is a no-op', () => { + const target = path.join(targetDir(), 'never-built'); + expect(pruneTarget(target, { now: NOW, log: () => {} })).toEqual({ + units: 0, + caches: 0, + bytes: 0, + }); + }); +}); + +describe('configuration', () => { + test('never runs in CI, and can be turned off', () => { + expect(targetGcEnabled({})).toBe(true); + expect(targetGcEnabled({ CI: 'true' })).toBe(false); + expect(targetGcEnabled({ RN_GPUI_TARGET_GC: '0' })).toBe(false); + }); + + test('keeps units for a week unless told otherwise', () => { + expect(targetGcMaxAgeDays({})).toBe(7); + expect(targetGcMaxAgeDays({ RN_GPUI_TARGET_GC_DAYS: '14' })).toBe(14); + expect(targetGcMaxAgeDays({ RN_GPUI_TARGET_GC_DAYS: 'nope' })).toBe(7); + }); +}); diff --git a/vendor/react-native-gpui/packages/cli/templates/app/App.tsx b/vendor/react-native-gpui/packages/cli/templates/app/App.tsx new file mode 100644 index 0000000000..12b5f735cc --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/App.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; + +export default function App() { + return ( + + REACT NATIVE + GPUI + Your desktop app is ready. + + Edit App.tsx and run `npx react-native run-desktop` to launch the GPUI + desktop target. + + + ); +} + +const styles = StyleSheet.create({ + page: { + flex: 1, + justifyContent: 'center', + padding: 48, + backgroundColor: '#10151d', + }, + eyebrow: { + color: '#70d6bd', + fontSize: 13, + fontWeight: '700', + letterSpacing: 2, + }, + title: { color: '#f5f7fa', fontSize: 34, fontWeight: '700', marginTop: 16 }, + body: { + color: '#bac3cf', + fontSize: 17, + lineHeight: 25, + marginTop: 12, + maxWidth: 520, + }, +}); diff --git a/vendor/react-native-gpui/packages/cli/templates/app/babel.config.js b/vendor/react-native-gpui/packages/cli/templates/app/babel.config.js new file mode 100644 index 0000000000..94781507ee --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/babel.config.js @@ -0,0 +1,10 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], + plugins: [ + // Rewrites `require` of a `.node` file (directly, or through a loader such + // as `node-gyp-build`, `bindings` or a napi-rs generated file) into the + // path the desktop runtime opens. A no-op on other platforms, so the same + // config still builds for iOS and Android. + 'module:@react-native-gpui/core/babel-plugin-node-addons', + ], +}; diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/.cargo/config.toml b/vendor/react-native-gpui/packages/cli/templates/app/desktop/.cargo/config.toml new file mode 100644 index 0000000000..650dca6817 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/.cargo/config.toml @@ -0,0 +1,6 @@ +[build] +target-dir = "build/target" + +[env] +RN_GPUI_AUTOLINK_MANIFEST = { value = "autolink/autolink.json", relative = true } +RN_GPUI_AUTOLINK_CMAKE = { value = "autolink/autolink.cmake", relative = true } diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/README.md b/vendor/react-native-gpui/packages/cli/templates/app/desktop/README.md new file mode 100644 index 0000000000..c9465567b1 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/README.md @@ -0,0 +1,128 @@ +# Desktop (GPUI) + +This is the app-local GPUI target. Its `Cargo.toml` depends on the `react-native-gpui` Rust crate; `src/main.rs` is the app's launch point, so you can add Rust modules and local crate dependencies without editing the platform package. iOS, Android, and desktop share the JavaScript entry point and JS dependencies. The build script reads the app name and `desktop.appId` from the project-root `app.json`. + +Run the desktop app from the project root with: + +```sh +npx react-native run-desktop +``` + +The default reuses this project's Metro on port 8081, or starts it in a new +terminal window (`--terminal ` or `$REACT_TERMINAL` picks the terminal), +and runs a debug Cargo build, so Fast Refresh and LogBox work. Change the Metro +port with `--port 8082`; `--no-packager` never starts Metro, and +`--packager-log` runs it in the background with its output in +`desktop/build/metro.log`. +Use `--release` to bundle JavaScript and run the optimized, Metro-free build. + +`npx react-native init-desktop` installs dependencies, autolinks native modules, and creates `build/index.bundle`; that bundle lets `cargo run` launch the latest initialized app without the CLI. For development with Fast Refresh, LogBox, and a live Metro server, use `npx react-native run-desktop`. + +Autolink output is generated under `desktop/autolink/` and gitignored. It is +recreated by `init-desktop` and each `run-desktop`; do not commit those generated +files. Cargo build output and bundles are also kept under `desktop/build/`. + +## Window configuration + +`src/main.rs` uses `DesktopAppConfig` to set the title and initial size. It can +also set an initial position, configure public GPUI `WindowOptions`, install +GPUI app services before launch, and access the live `gpui::Window` after it +opens. Window size and position restore from the previous run by default; call +`.restore_window_state(false)` to always use the configured initial geometry. + +## Project-local Rust TurboModules + +A local Rust TurboModule is a spec file plus a `mod` that implements the trait +generated from it: + +1. Add `src/NativeGreeting.ts` with a + `TurboModuleRegistry.getEnforcing('NativeGreeting')` spec. +2. List it in `desktop/build.rs`: + `rn_gpui_module_build::generate(&["src/NativeGreeting.ts"]);`. +3. Write the implementation in `desktop/src/greeting.rs`, deriving nothing by + hand: the generated file gives you `NativeGreetingSpec`, the events handle and + the value structs. + + ```rust + use rn_gpui_module::{ModuleError, ModuleThread, Value}; + + pub struct Greeting; + + impl NativeGreetingSpec for Greeting { + fn new(_events: NativeGreetingEvents) -> Self { + Self + } + + fn greet(&self, name: String) -> Result { + Ok(format!("Hello from Rust, {name}!")) + } + } + ``` + + A method that returns a value runs on the JS thread; a method that returns + `void` or a `Promise` runs on the module's own thread unless the impl adds + `const THREAD: ModuleThread = ModuleThread::Main;`. + +4. Declare `mod greeting;` in `src/main.rs` and register it before the app + starts: + + ```rust + react_native_gpui::register_module(native_greeting_module::()); + ``` + +`include_specs!()` in `main.rs` pulls in the generated traits, structs and +descriptors. A mistake against the spec is a compile error in this crate, not a +runtime surprise. The runtime crate is `rn-gpui-module`, and +`rn-gpui-module-build` runs the generator from `build.rs`. + +A Rust module depends on core and on gpui by **version**, never by path: + +```toml +[dependencies] +rn-gpui-module = "0.1" +gpui = "0.2" + +[build-dependencies] +rn-gpui-module-build = "0.1" +``` + +Those crates live inside `@react-native-gpui/core`, at a path that only exists +in this repository, so a relative path breaks the moment the package is +installed. Autolink reads core's `Cargo.toml` for the gpui revision and writes a +`[patch.crates-io]` table into the app's `desktop/Cargo.toml`, which is the only +place `[patch]` applies; that table points each crate at the core the app +actually installed and pins gpui to core's revision. It is regenerated on every +autolink run, so edit the version in your own manifest and leave the patch +alone. A path or git dependency on those crates is refused with an explanation +rather than failing later in an installed app. + +For a reusable TurboModule library, keep its TypeScript spec under `src/` and +its native implementation under `desktop/`, with `codegenConfig` and +`platforms.desktop` in `package.json`; `npx react-native create-library` creates +that layout. Autolink runs React Native Codegen over the package's specs and +writes `JSI.h` plus `.h`, the latter +declaring one spec class per spec file (`src/NativeGreeting.ts` defines +`NativeGreetingSpec`). A C++ implementation derives from that class and +declares one method per spec method, with typed arguments (`std::string`, +`double`, `bool`, `folly::dynamic`, `std::vector`, `std::optional`, +`rngpui::Callback<...>`) and either a return value or, for a spec `Promise`, +a `void` method taking `rngpui::Promise` last. `react/bridging` performs the +conversion, so a signature that disagrees with the spec fails to compile. + +Methods that return a value run on the JS thread and take `jsi::Runtime&` +first. Methods that return `void` or a `Promise` run on the module's own thread +by default, so they never block the JS thread; set +a `public static constexpr rngpui::ModuleThread kThread` to `Main` (the GPUI +main thread) or `JS` (inline on the JS thread, taking `jsi::Runtime&` first) to +choose another thread; the generated spec class reads `T::kThread`, so it has to +be public. A method that does not run on the JS thread cannot take +a JSI type. A spec `readonly onProgress: CodegenTypes.EventEmitter` +generates a protected `emitOnProgress(value)` that any thread may call, and +`sendEvent("name", body)` covers modules used through `NativeEventEmitter`. + +`desktop/desktop.json` is the app's own configuration: its name, identifier, +version, icon, window and how it is packaged. Its schema is +`@react-native-gpui/cli/desktop.schema.json`, so an editor completes it. A +`desktop..json` beside it is merged over the base for that OS. The +`Info.plist` is generated from these settings rather than hand-written, and +`npx react-native package-desktop` builds the installers from the same file. diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/assets/README.md b/vendor/react-native-gpui/packages/cli/templates/app/desktop/assets/README.md new file mode 100644 index 0000000000..417a9a4a45 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/assets/README.md @@ -0,0 +1,4 @@ +# Desktop assets + +Point `desktop.json`'s `icon` at one 1024×1024 PNG; the per-OS formats (`.icns`, +`.ico`, PNG sizes) are generated from it when the app is packaged. diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/assets/icon.png b/vendor/react-native-gpui/packages/cli/templates/app/desktop/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..576f7080edf3927b020760f2a8869318318e991b GIT binary patch literal 8867 zcmeHNYg|)TwqNHYkRVTiDhLRQfLf7IAD{*jt)iBKw05kmRE`(NN=1pH@({_Uf{%(Y z?X9Bx~>W z*#Euu+UKmjcJ_sZF18#sZWI7mE?u(lWdI?PLJ*tckJ9DYT>ul`TDoxlEAhFVjRA$u zwJE#Tyd1X4D`=N*St^sh?@dRmH_cw(b5N3>)e_Hf2PGK?cH8YRnYsRNH6GiPeV>1J zX-;?VT0zu*-|iaoS$R?D8>f|``32UlGM9lF0E*n+2z`Ik?PUk71A=`!7>J*gPd*UC zi&bYuzzMIz$}5Na0T*j311P;Xg;9Hg z%IsrTfK6VeDien6k?0uU=DiJ>ZlIV!8TY+R70)u{n8N^o#LAa!5JTy?n<5}R`6p8Blo}H&ZuAEvdH#2v{J`0-MOY0)U9Uj5jM!ZNc8s zh2r%#Fy>7ic4yhLHJ&i-EZT-H57Xm|VdY*bBmK}uWyNsJZX8E7YXX%khHo2Ojhm8XczMHlh93M zY5|im0XnL+h*-DJM&*+U9cOjeP`#Tm5jwsxk{@LJG@2ZsAR((~39rtDy1(?`w7BOF zoG~Mo4wUg=^EgdUF2oy$;ML>a1+Bai(UJqRre=baP&cT&GldP8&B#287V1xyb$LO< z*CHFVB%58g69Ecb=b}nv!X!;%D1@J*qx&D;xIKW;j0)sXr&aZRp*w`16`7-sRDJCR z&fQNZVQ~4r?ly@(WCx#1^w?@!Q&AQQ@hR3;=$54Dk}Na0tW7~ic2>v(DWh=;oQ&3b zPj)6Rh4?cfjDOB$c2lj8D0{v}#>&hS4`wjDh{gV>iTtuVNTV{hlSXmb{$LTHAAw{n zm$GXG@__>gbMoA@BJMeOzgdBduaXku8Q#{OnQTcJ9QS%?}|g!5xPwZk(cW$+ChpKlBW#^7lSHA8SdyO@);c1SwmP=8k&Li^gAo zG1lNd)((m6%~!0(LeS29q`9_@^TH|D$R1;h#L6|TR(24yV-3>WCnx4D1czHDIBW8Z zU$+hZqJ$LEUtU1jH#&F3(;lO?JvtmFM{Bx2GgP45k$&}3Xq+n=R%|y~VYiz>m!A6ei<}>a zY)A)fKvhh8h=9;d1(DI~Gw}!H{5pnjR3hc@X7D^CY$j(22X{QAldc>HrNb6lH*WBv zJ#L!(VYS%EAvNRhLg_T{*V7R)@0~p{tUhT7Kz;lVZ9_1-tcGB!$A3|Fh}tAz9G>kO z1sMi9fxxatO(^Ba`HMrN)zt>9Y8adq^&;Xp3L2dZDd*Ojkgh?r;V#fHfV+i6HvZ4W zr%V?Pn|3H{yg6u^P&2@c6+spCJvXg3dfVU><&;6lewMUS4@-^)`< zzQ^dfdXM_XCqc<6pCp2|pCYSnIbqp7h)hfwVOB%)3twG-=P3|I556KG?M5V_J{uFC zL^9+aqov8FP!7}sZTB10af1tQWQYgdBBuk9*FkGewK&V>O-cXC)S$)@d+0Y#L3OZT zRr7Br^W@+|8n)0sn&2?1qdqx*NW)Ez(lETtgK*=OecGNY-^*xA<^At5{+|Am)BP>R z5skqu_1n-G@fD55r*_MTOa8b^NOz{5Ax8~;Erv|}XwZZy`+JI-81B{XWv+qA*B(Te z**o@wcJ6$r`w$?y@}`K??`8NgLeS)=Cqw)+Ux>HItXy2-3Th7_Jh)Q;5H~~5a61Z% z__AEgv)lHI!MP#|G-L1?>F29wH2#GZyf<2S>-1LOd%`nQHY5n4INN6&NIWxzkfmUu zcrU))TDKfBojw5|=FTT9RJJEtV)_lMZ(I;Ub;UNP_ycsBz_6g57&jWYz`|xe7_Gi` zS_*t=cvVWf7N5<22^OJ4}<; z-~wb@$t6La@$otImOxGaF=AlG3*htvTln~4g>|?7ZcTIVBiH7EO?i2kyw$h?EVneo z7*-=_nBJ?u+tONKZdlFDcXt+*uG5t-6a%@?-&56`_^I1@GvI!1F?7m;@nu5VA9@h} zZmTj=IUQd#=&0>Q_m+%BNSZ7>YR$X;&`?%cf`YAjy{pE71yxSjV%i3o@l8gbj|WdX zbz{B+{R{C6=+k+netQ4+hGa|$QfL=-)nAGq1)jf*b{CJPJ-`24b?Y$b+hsH!$e|Mb z^I24qD&t3JeDk48gZ^)Nl$seBOhwlx`(a+L*?1NN70o_O zs;3u_VNWnL_*6n-C0YX?v z?R13G9%2ZK`)S4i3p34doV7TS40n{5VV-TA6Mm08JgF~e)Zz!189yT~pTHSzQ- zz!!%Xq+m|Wf16Z-`A%`5APbEi)4j()tKtpQ!U+Jc_Wl`@r}jjP@L)V&T*m^dbJaG7 z;xvGU>MU!h%e(EPIMyAbHwfeJc>IJ(;DK5J%wDS#JlMkGQ6=I9Ech3mCO#&=(6`$Lu{n}E{Z4KR!=P%^ReL=T~AR#GgSC<*jfxnzpv3oGKunZYtbY^T~ z)Ro_w!Xjoet@1+kjW4~Qs%}~X9(8fg&nf+~<*DMU5tulB2IvB)qC_36e!8Bzh~XoR zoZnPhza8cr+@SvINyPl0hB>=8_&v?JI>b>NU!^V?)KM%41-+2OTZ`MCPr0&q4#Znf z&i!qd#|&|>4joEY>WMlup(4P>XMqHLq+r1=0lMDS6v&d(bmlb zf8&`fGWzDlWw4i8Q1oxVZ(MI-hb`pNZ9yXaP7V&ES~cu~`tw653mJY2Rr6ox-kr?K z=`tPZD(%%7s*Mj~8^w}A74lgx6K9ac8uuA%H3TIrHt=Sbp~VXpQX5k*i82hArA85^ z`KmdEVXy9Pq27YMLNfXV+H<@#VUHNx%iNGwU9WNRg!KDokmg@-iu*hM0xV?t^=TBz ziaK?L33(}EGSb-zSF9WFBKR=Lp}+rSFFUom6sFF zY@GWBWZz4}8VzUJzSQs5`wZ7M))@gL9P(=V=!z-%F@oOfv~4+@v@6sFDGDvSj7LIp zx|gkw?E}?$C!9Q=eQAI6B@dQ=A%;4j)!*kIv#<MYX@(|(8+#2uWI&xO$GaA4jajz7F)(LNBV+r&wkgGdJe*p z>7~^9OD*2nmT=E+JO)FXb2GUG+Sdl?z7A8U2ZZ2?YeT+tG|0~7f$og)3Nt|6bXiEe zk|Zcpv!-=t20)J0U95%Y&~M3i2&;#4juOk8maWOLp6+R;$>lK@vnA+7I7OcwDj z{i{)j>~z!@#j+Z!K)NuGzWBP!9U9IVZ+~~FYX_vPRz9BYocL)KhRt}B?B@C+wwyM+ z!t%TJAqDEId!?*u0@f1&!mC2AcKS1_39jK(=q>wDysVBwlC+m3d}Zwg+ph~OF*crc zSqgRU(uL*RUA}b(QO^>j!)~BlG7G~! zG3>WPWfJT-u)z){QY0FW-O62H<4(O0;h^_s<=Hg+0)muTKqvu3R%E(%>(xPW{6 Fe*qyx8vg(Q literal 0 HcmV?d00001 diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/build.rs b/vendor/react-native-gpui/packages/cli/templates/app/desktop/build.rs new file mode 100644 index 0000000000..8f1092343c --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/build.rs @@ -0,0 +1,172 @@ +fn main() { + // Forwards the engine's Node-API exports out of this executable, which is + // what lets a stock `.node` addon resolve them on Windows. + // + // A DLL records every import as a name *plus the file it comes from*, and a + // node-gyp addon records "from `node.exe`" — Windows has no global symbol + // scope for it to fall back on. Node's own answer is that the host process + // exports the Node-API functions, so that is what this does: the executable + // re-exports each of the VM's `napi_*`/`node_api_*` names, forwarded to + // `hermesvm.dll`. Addons that use node-gyp's delay-load hook (the default) + // then reach this process instead of looking for `node.exe` on disk. + // + // It is a no-op on macOS and Linux, where an addon's imports resolve against + // whatever is already loaded and need no forwarding. Declare it once, before + // the module list; it does not depend on anything else in this file. + rn_gpui_module_build::forward_node_api_exports(); + + // App-local Rust TurboModules: list one entry per `src/Native*.ts` spec, + // for example `&["src/NativeGreeting.ts"]`. Their glue is generated into + // this crate's OUT_DIR, and main.rs registers the modules. + rn_gpui_module_build::generate(&[]); + + let desktop_root = std::path::PathBuf::from( + std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"), + ); + // `app.json` names the app; `desktop/desktop.json` carries the identifier + // and everything else the packaging step needs. The loader the CLI uses is + // JavaScript, so this reads the same file directly and applies the two + // defaults it would apply. + let project_root = desktop_root + .parent() + .expect("desktop folder has a React Native project root"); + let app_json_path = project_root.join("app.json"); + println!("cargo:rerun-if-changed={}", app_json_path.display()); + let app_json: serde_json::Value = serde_json::from_slice( + &std::fs::read(&app_json_path) + .unwrap_or_else(|error| panic!("read {}: {error}", app_json_path.display())), + ) + .unwrap_or_else(|error| panic!("parse {}: {error}", app_json_path.display())); + let app_name = app_json["name"] + .as_str() + .filter(|name| !name.is_empty()) + .expect("app.json must define a nonempty name"); + if app_json["desktop"] + .as_object() + .is_some_and(|desktop| !desktop.is_empty()) + { + panic!("move `app.json` desktop.* into `desktop/desktop.json` (see the README)"); + } + + let desktop_json_path = project_root.join("desktop/desktop.json"); + println!("cargo:rerun-if-changed={}", desktop_json_path.display()); + let desktop_json: serde_json::Value = std::fs::read(&desktop_json_path) + .ok() + .and_then(|bytes| serde_json::from_slice(&bytes).ok()) + .unwrap_or(serde_json::Value::Null); + let app_id = match desktop_json["identifier"].as_str() { + Some(identifier) if !identifier.is_empty() => identifier.to_string(), + _ => format!( + "com.example.{}", + app_name + .to_lowercase() + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) + .collect::() + ), + }; + println!("cargo:rustc-env=RN_GPUI_APP_NAME={app_name}"); + println!("cargo:rustc-env=RN_GPUI_APP_ID={app_id}"); + // Only a debug build names the bundle it was developed against. A release + // binary must find the bundle beside its own executable, because the path + // baked in here is where this machine happened to build it — and on the + // machine the package is installed on, that path does not exist. + println!("cargo:rerun-if-env-changed=PROFILE"); + if std::env::var("PROFILE").as_deref() == Ok("debug") { + println!( + "cargo:rustc-env=RN_GPUI_DEFAULT_BUNDLE={}", + desktop_root.join("build/index.bundle").display() + ); + } + + println!("cargo:rerun-if-env-changed=DEP_RN_GPUI_RUNTIME_RPATHS"); + println!("cargo:rerun-if-env-changed=RN_GPUI_PACKAGING"); + let Ok(paths) = std::env::var("DEP_RN_GPUI_RUNTIME_RPATHS") else { + return; + }; + let dirs = paths.split('|').filter(|path| !path.is_empty()); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + stage_shared_libraries(dirs); + return; + } + // `RN_GPUI_PACKAGING` means this binary is about to be installed somewhere + // else, so only the relative rpaths belong in it. Keeping the absolute ones + // would let a packaged app load this build tree's libraries in preference to + // its own on the machine that built it — which hides exactly the packaging + // bugs the packaged run exists to catch. + let packaging = std::env::var("RN_GPUI_PACKAGING").is_ok(); + if !packaging { + for path in dirs { + println!("cargo:rustc-link-arg=-Wl,-rpath,{path}"); + } + } + emit_relocatable_rpaths(app_name); +} + +/// rpaths that survive being installed somewhere else. +/// +/// The absolute paths above let a development build run from this tree. A +/// packaged app is copied to another machine, where those paths do not exist, +/// so a release build also carries paths relative to the executable: the +/// layout the installers produce. +fn emit_relocatable_rpaths(app_name: &str) { + if std::env::var("PROFILE").as_deref() != Ok("release") { + return; + } + let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let mut emit = |rpath: String| println!("cargo:rustc-link-arg=-Wl,-rpath,{rpath}"); + match os.as_str() { + // An app bundle puts the executable in Contents/MacOS and the shipped + // libraries in Contents/Frameworks. + "macos" => emit("@executable_path/../Frameworks".into()), + // Linux installs the executable at `usr/bin` and ships its libraries + // under `usr/lib`: + // - the deb puts them in `usr/lib/`; + // - an AppImage's libraries are flat in `usr/lib`, because that is the + // directory AppRun puts on the library path and the one linuxdeploy + // resolves dependencies from before the image is assembled. + // Both are covered, so one binary works in either layout. + "linux" => { + emit(format!("$ORIGIN/../lib/{app_name}")); + emit("$ORIGIN/../lib".into()); + emit("$ORIGIN".into()); + } + // Windows has no rpath: the DLLs are copied beside the executable. + _ => {} + } +} + +/// Windows has no rpath: the loader finds a DLL next to the executable or on +/// `PATH`, so every directory the runtime reports as holding a shared library +/// (the Hermes binary directory in particular) is copied into the profile +/// directory that receives this app's executable. Without this the app builds +/// and then fails to start with "hermesvm.dll was not found". +fn stage_shared_libraries<'a>(dirs: impl Iterator) { + let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is set")); + // /build/-/out -> + let Some(profile_dir) = out_dir.ancestors().nth(3) else { + return; + }; + for dir in dirs { + let Ok(entries) = std::fs::read_dir(dir) else { + continue; + }; + for entry in entries.flatten() { + let source = entry.path(); + let is_shared_library = source + .extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| extension.eq_ignore_ascii_case("dll")); + if !is_shared_library { + continue; + } + let Some(name) = source.file_name() else { + continue; + }; + println!("cargo:rerun-if-changed={}", source.display()); + std::fs::copy(&source, profile_dir.join(name)).unwrap_or_else(|error| { + panic!("copy {} next to the executable: {error}", source.display()) + }); + } + } +} diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/desktop.json b/vendor/react-native-gpui/packages/cli/templates/app/desktop/desktop.json new file mode 100644 index 0000000000..a68c4834b5 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/desktop.json @@ -0,0 +1,9 @@ +{ + "bundle": { + "targets": "all", + "macos": { + "minimumSystemVersion": "13.0" + } + }, + "icon": "assets/icon.png" +} diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/e2e/app.e2e.ts b/vendor/react-native-gpui/packages/cli/templates/app/desktop/e2e/app.e2e.ts new file mode 100644 index 0000000000..10b1f3f7ca --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/e2e/app.e2e.ts @@ -0,0 +1,17 @@ +import { beforeAll, expect, test } from 'vitest'; +import { launchApp, type DesktopApp } from '@react-native-gpui/testing'; + +let app: DesktopApp; +beforeAll(async () => { + app = await launchApp({ + disableAnimations: true, + idleOptions: { ignore: ['animations'] }, + }); +}); + +test('renders the React Native app', async () => { + await app.waitForIdle(); + // Existing React Native apps do not need to add a testID to adopt desktop. + const tree = (await app.tree()) as { children: unknown[] }; + expect(tree.children.length).toBeGreaterThan(0); +}); diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/gitignore b/vendor/react-native-gpui/packages/cli/templates/app/desktop/gitignore new file mode 100644 index 0000000000..2e460e95bb --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/gitignore @@ -0,0 +1,3 @@ +build/ +.cache/ +autolink/ diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/metro.config.js b/vendor/react-native-gpui/packages/cli/templates/app/desktop/metro.config.js new file mode 100644 index 0000000000..b799517712 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/metro.config.js @@ -0,0 +1,6 @@ +const path = require('path'); +const { makeMetroConfig } = require('@react-native-gpui/core/metro.config'); + +module.exports = makeMetroConfig({ + projectRoot: path.resolve(__dirname, '..'), +}); diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/src/main.rs b/vendor/react-native-gpui/packages/cli/templates/app/desktop/src/main.rs new file mode 100644 index 0000000000..8f2300ba5b --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/src/main.rs @@ -0,0 +1,41 @@ +//! Project-owned desktop launch configuration. +//! +//! A local Rust TurboModule is a `Native*.ts` spec beside this file plus a +//! `mod` that implements the trait generated from it. `build.rs` lists the +//! specs, and `main` registers each module before the app starts. See +//! `desktop/README.md` for the full example. + +rn_gpui_module::include_specs!(); + +// mod greeting; +// use greeting::{native_greeting_module, Greeting}; + +fn main() { + // A debug build knows where its own bundle is; a release build does not, and + // must not: the path would name the machine that built it. A packaged release + // app resolves the bundle beside its executable instead (see `resources`). + #[cfg(debug_assertions)] + if std::env::var_os("RN_GPUI_BUNDLE").is_none() { + std::env::set_var("RN_GPUI_BUNDLE", env!("RN_GPUI_DEFAULT_BUNDLE")); + } + // App-local modules are registered before the app starts; a module the + // platform already provides would panic here. + // react_native_gpui::register_module(native_greeting_module::()); + + let config = react_native_gpui::DesktopAppConfig::from_env_with_defaults( + env!("RN_GPUI_APP_NAME"), + env!("RN_GPUI_APP_ID"), + ) + .window_title(env!("RN_GPUI_APP_NAME")) + .window_size(1024.0, 768.0) + // Uncomment to choose a fixed initial top-left location (screen DIPs). + // .window_position(120.0, 80.0) + // .restore_window_state(false) + .configure_window(|options| { + // GPUI window options are public: titlebar, resizability, display, + // decorations, min size, and other native window behavior. + options.is_resizable = true; + }); + + react_native_gpui::run_with(config); +} diff --git a/vendor/react-native-gpui/packages/cli/templates/app/desktop/vitest.e2e.config.ts b/vendor/react-native-gpui/packages/cli/templates/app/desktop/vitest.e2e.config.ts new file mode 100644 index 0000000000..187439e21f --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/app/desktop/vitest.e2e.config.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url'; +import { defineConfig, mergeConfig } from 'vitest/config'; +import desktopPreset from '@react-native-gpui/testing/vitest-preset-e2e'; + +const projectRoot = fileURLToPath(new URL('..', import.meta.url)); +process.env.RN_GPUI_TEST_PROJECT = projectRoot; + +export default mergeConfig( + defineConfig(desktopPreset), + defineConfig({ + root: projectRoot, + test: { include: ['desktop/e2e/**/*.e2e.ts'] }, + }), +); diff --git a/vendor/react-native-gpui/packages/cli/templates/modules/cpp/CMakeLists.txt b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/CMakeLists.txt new file mode 100644 index 0000000000..b1a45754f3 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/CMakeLists.txt @@ -0,0 +1,3 @@ +set_property( + GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/cpp/RNModule.cpp") diff --git a/vendor/react-native-gpui/packages/cli/templates/modules/cpp/NativeModule.ts b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/NativeModule.ts new file mode 100644 index 0000000000..ae8ddf7bc1 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/NativeModule.ts @@ -0,0 +1,7 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { + greet(name: string): string; +} + +export default TurboModuleRegistry.getEnforcing('RNModule'); diff --git a/vendor/react-native-gpui/packages/cli/templates/modules/cpp/RNModule.cpp b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/RNModule.cpp new file mode 100644 index 0000000000..47a9efd916 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/RNModule.cpp @@ -0,0 +1,29 @@ +#include "RNModule.hpp" + +#include + +#include +#include +#include + +namespace { +namespace jsi = facebook::jsi; +using facebook::react::CallInvoker; +using facebook::react::TurboModule; + +class RNModule final : public facebook::react::NativeModuleSpec { + public: + explicit RNModule(std::shared_ptr jsInvoker) + : NativeModuleSpec(std::move(jsInvoker)) {} + + std::string greet(jsi::Runtime&, std::string name) { + return "Hello from C++, " + name + "!"; + } +}; +} // namespace + +std::shared_ptr create_rn_module( + const std::string& name, + std::shared_ptr jsInvoker) { + return rngpui::provideTurboModule(name, std::move(jsInvoker)); +} diff --git a/vendor/react-native-gpui/packages/cli/templates/modules/cpp/RNModule.hpp b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/RNModule.hpp new file mode 100644 index 0000000000..3c40146aa6 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/RNModule.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace facebook::react { +class CallInvoker; +class TurboModule; +} // namespace facebook::react + +std::shared_ptr create_rn_module( + const std::string& name, + std::shared_ptr jsInvoker); diff --git a/vendor/react-native-gpui/packages/cli/templates/modules/cpp/index.ts b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/index.ts new file mode 100644 index 0000000000..c0550abf51 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/modules/cpp/index.ts @@ -0,0 +1 @@ +export { default } from './NativeModule'; diff --git a/vendor/react-native-gpui/packages/cli/templates/modules/rust/NativeModule.ts b/vendor/react-native-gpui/packages/cli/templates/modules/rust/NativeModule.ts new file mode 100644 index 0000000000..ae8ddf7bc1 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/modules/rust/NativeModule.ts @@ -0,0 +1,7 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { + greet(name: string): string; +} + +export default TurboModuleRegistry.getEnforcing('RNModule'); diff --git a/vendor/react-native-gpui/packages/cli/templates/modules/rust/build.rs b/vendor/react-native-gpui/packages/cli/templates/modules/rust/build.rs new file mode 100644 index 0000000000..f75076328e --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/modules/rust/build.rs @@ -0,0 +1,3 @@ +fn main() { + rn_gpui_module_build::generate(&["../src/NativeModule.ts"]); +} diff --git a/vendor/react-native-gpui/packages/cli/templates/modules/rust/index.ts b/vendor/react-native-gpui/packages/cli/templates/modules/rust/index.ts new file mode 100644 index 0000000000..c0550abf51 --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/modules/rust/index.ts @@ -0,0 +1 @@ +export { default } from './NativeModule'; diff --git a/vendor/react-native-gpui/packages/cli/templates/modules/rust/lib.rs b/vendor/react-native-gpui/packages/cli/templates/modules/rust/lib.rs new file mode 100644 index 0000000000..243c8839bf --- /dev/null +++ b/vendor/react-native-gpui/packages/cli/templates/modules/rust/lib.rs @@ -0,0 +1,26 @@ +//! A Rust TurboModule: the trait, the events handle and the value structs come +//! from the spec file next to it. + +rn_gpui_module::include_specs!(); + +use rn_gpui_module::ModuleError; + +pub struct RNModule; + +impl NativeModuleSpec for RNModule { + fn new(_events: NativeModuleEvents) -> Self { + Self + } + + /// A method that returns a value runs on the JS thread. A method that + /// returns `void` or a `Promise` runs on the module's own thread unless the + /// impl adds `const THREAD: ModuleThread = ModuleThread::Main;`. + fn greet(&self, name: String) -> Result { + Ok(format!("Hello from Rust, {name}!")) + } +} + +/// Called by the app's autolink shim, which keeps these modules linked in. +pub fn rn_gpui_register() { + rn_gpui_module::register(native_RNMODULESNAKE_module::()); +} diff --git a/vendor/react-native-gpui/packages/codegen/package.json b/vendor/react-native-gpui/packages/codegen/package.json new file mode 100644 index 0000000000..ea1f3a0cfc --- /dev/null +++ b/vendor/react-native-gpui/packages/codegen/package.json @@ -0,0 +1,21 @@ +{ + "name": "@react-native-gpui/codegen", + "version": "0.1.0", + "description": "C++ and Rust code generators for react-native-gpui, plugged into @react-native/codegen.", + "files": [ + "src" + ], + "type": "module", + "exports": { + "./generate-cxx-spec": "./src/generate-cxx-spec.js", + "./generate-rust-spec": "./src/generate-rust-spec.js", + "./generate-host-component-cpp": "./src/generate-host-component-cpp.js" + }, + "scripts": { + "generate-cxx-spec": "node ./src/generate-cxx-spec.js", + "generate-rust-spec": "node ./src/generate-rust-spec.js" + }, + "dependencies": { + "@react-native/codegen": "0.87.1" + } +} diff --git a/vendor/react-native-gpui/packages/codegen/src/generate-cxx-spec.js b/vendor/react-native-gpui/packages/codegen/src/generate-cxx-spec.js new file mode 100644 index 0000000000..585b4b3c4b --- /dev/null +++ b/vendor/react-native-gpui/packages/codegen/src/generate-cxx-spec.js @@ -0,0 +1,341 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; + +const toPascalCase = value => + value.replaceAll(/(?:^|[^A-Za-z0-9]+)([A-Za-z0-9])/gu, (_match, character) => + character.toUpperCase(), + ); + +/** + * The C++ type an implementation declares for one spec type, plus the JSI type + * it converts to. A named object type maps to the struct React Native's own + * generator emits for that type alias in `JSI.h`. + */ +function cppTypeOf(annotation, context) { + const { hasteModuleName, aliasMap, fail } = context; + const recurse = inner => cppTypeOf(inner, context); + switch (annotation.type) { + case 'StringTypeAnnotation': + case 'StringLiteralTypeAnnotation': + return { cpp: 'std::string', jsi: 'jsi::String' }; + case 'NumberTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + case 'NumberLiteralTypeAnnotation': + return { cpp: 'double', jsi: 'double' }; + case 'Int32TypeAnnotation': + return { cpp: 'int32_t', jsi: 'int' }; + case 'BooleanTypeAnnotation': + case 'BooleanLiteralTypeAnnotation': + return { cpp: 'bool', jsi: 'bool' }; + case 'GenericObjectTypeAnnotation': + case 'ObjectTypeAnnotation': + return { cpp: 'folly::dynamic', jsi: 'jsi::Object' }; + case 'MixedTypeAnnotation': + return { cpp: 'folly::dynamic', jsi: 'jsi::Value' }; + case 'ArrayBufferTypeAnnotation': + // Copied, so a module-thread method never holds a runtime-bound object. + return { cpp: 'std::vector', jsi: 'jsi::ArrayBuffer' }; + case 'VoidTypeAnnotation': + return { cpp: 'void', jsi: 'jsi::Value' }; + case 'ArrayTypeAnnotation': { + const element = recurse(annotation.elementType); + return { cpp: `std::vector<${element.cpp}>`, jsi: 'jsi::Array' }; + } + case 'FunctionTypeAnnotation': + return { + cpp: `rngpui::Callback<${annotation.params + .map(parameter => cppParameterOf(parameter, context)) + .join(', ')}>`, + jsi: 'jsi::Function', + }; + case 'NullableTypeAnnotation': { + const inner = recurse(annotation.typeAnnotation); + return { cpp: `std::optional<${inner.cpp}>`, jsi: inner.jsi }; + } + case 'ReservedTypeAnnotation': + if (annotation.name === 'RootTag') { + return { cpp: 'double', jsi: 'double' }; + } + return fail(`ReservedTypeAnnotation(${annotation.name})`); + case 'EnumDeclaration': + switch (annotation.memberType) { + case 'NumberTypeAnnotation': + return { cpp: 'double', jsi: 'jsi::Value' }; + case 'StringTypeAnnotation': + return { cpp: 'std::string', jsi: 'jsi::String' }; + default: + return fail(`EnumDeclaration(${annotation.memberType})`); + } + case 'UnionTypeAnnotation': { + const memberTypes = new Set(annotation.types.map(member => member.type)); + if (memberTypes.size !== 1) { + return fail('UnionTypeAnnotation with mixed member types'); + } + switch ([...memberTypes][0]) { + case 'StringLiteralTypeAnnotation': + return { cpp: 'std::string', jsi: 'jsi::String' }; + case 'NumberLiteralTypeAnnotation': + return { cpp: 'double', jsi: 'double' }; + case 'BooleanLiteralTypeAnnotation': + return { cpp: 'bool', jsi: 'bool' }; + default: + return fail('UnionTypeAnnotation'); + } + } + case 'TypeAliasTypeAnnotation': { + const alias = aliasMap?.[annotation.name]; + if (!alias) { + return fail(`unresolved type alias ${annotation.name}`); + } + if ( + alias.type === 'ObjectTypeAnnotation' && + alias.properties.length > 0 + ) { + return { + cpp: `${hasteModuleName}${annotation.name}`, + jsi: 'jsi::Object', + }; + } + return recurse(alias); + } + default: + return fail(annotation.type); + } +} + +/// The C++ type an implementation declares for one parameter: a parameter the +/// spec marks optional may be omitted, so it is `std::optional`. +const cppParameterOf = (parameter, context) => { + const { cpp } = cppTypeOf(parameter.typeAnnotation, context); + return parameter.optional && + parameter.typeAnnotation.type !== 'NullableTypeAnnotation' + ? `std::optional<${cpp}>` + : cpp; +}; + +const isObjectType = annotation => + annotation.type === 'ObjectTypeAnnotation' || + annotation.type === 'GenericObjectTypeAnnotation'; + +function methodOf(name, typeAnnotation, context) { + const method = `${context.location}: ${name}`; + const returnType = typeAnnotation.returnTypeAnnotation; + const kind = + returnType.type === 'VoidTypeAnnotation' + ? 'void' + : returnType.type === 'PromiseTypeAnnotation' + ? 'promise' + : 'value'; + if (name === 'getConstants' && kind === 'promise') { + throw new Error(`${method}: getConstants cannot return a Promise`); + } + if ( + kind === 'value' && + typeAnnotation.params.some( + parameter => parameter.typeAnnotation.type === 'FunctionTypeAnnotation', + ) + ) { + throw new Error( + `${method}: a method that takes a callback must return void or a Promise`, + ); + } + const parameters = typeAnnotation.params.map(parameter => ({ + ...parameter, + context, + })); + const described = parameters + .map(parameter => `${cppParameterOf(parameter, context)} ${parameter.name}`) + .join(', '); + // A value method runs on the JS thread, so an object return may stay a JSI + // object; everything else uses the converted type. + const valueReturn = () => + isObjectType(returnType) + ? 'jsi::Object' + : cppTypeOf(returnType, context).cpp; + const promiseType = () => { + const element = returnType.elementType; + return `rngpui::Promise<${ + element && element.type !== 'VoidTypeAnnotation' + ? cppTypeOf(element, context).cpp + : '' + }>`; + }; + const signature = + kind === 'value' + ? `${valueReturn()} ${name}(jsi::Runtime& rt${described ? `, ${described}` : ''})` + : kind === 'void' + ? `void ${name}(${described})` + : `void ${name}(${described ? `${described}, ` : ''}${promiseType()})`; + // A value method always takes the runtime; a void or Promise method takes it + // only when the module asks for the JS thread, and a Promise method takes the + // promise last. + const countParts = []; + if (kind === 'value') { + countParts.push('1'); + } else { + countParts.push('(T::kThread == rngpui::ModuleThread::JS ? 1 : 0)'); + } + if (parameters.length > 0) { + countParts.push(`${parameters.length}`); + } + if (kind === 'promise') { + countParts.push('1'); + } + const parameterCount = countParts.join(' + '); + const registration = + kind === 'value' + ? `valueMethod` + : kind === 'void' + ? `voidMethod` + : `promiseMethod`; + return { + name, + parameterCount, + message: + kind === 'value' + ? `${name} must be declared as ${signature}` + : `${name} must be declared as ${signature}; with kThread = JS it takes jsi::Runtime& rt first`, + registration: `this->template ${registration}<&T::${name}>("${name}");`, + }; +} + +function eventOf(eventEmitter, context) { + const payload = eventEmitter.typeAnnotation.typeAnnotation; + const name = eventEmitter.name; + const emitName = `emit${toPascalCase(name)}`; + if (payload.type === 'VoidTypeAnnotation') { + return { + registration: `this->template addEvent<>("${name}");`, + emitter: ` void ${emitName}() { + this->emitEvent("${name}"); + }`, + }; + } + const type = cppTypeOf(payload, context); + return { + registration: `this->template addEvent("${name}");`, + emitter: ` template + void ${emitName}(V value) { + static_assert( + facebook::react::bridging::supportsToJs, + "${name}: the value cannot be converted to the event's JS type"); + this->emitEvent("${name}", std::move(value)); + }`, + }; +} + +/** + * The spec header for one codegen library: a `Spec` class per + * NativeModule that registers its methods, typed events and emitters with + * `rngpui::TurboModuleBase`. + */ +export function generateCxxSpec({ libraryName, schema, specPaths = {} }) { + const modules = Object.entries(schema.modules ?? {}).filter( + ([, module]) => module.type === 'NativeModule', + ); + const classes = modules.map(([hasteModuleName, module]) => { + const location = specPaths[hasteModuleName] ?? hasteModuleName; + // `where` is the method or event emitter a failing type came from, so the + // error names it and not just the spec file. + const contextFor = where => ({ + hasteModuleName, + aliasMap: module.aliasMap, + location, + where, + fail: typeName => { + throw new Error( + `${location}${where ? `: ${where}` : ''}: unsupported spec type ${typeName}`, + ); + }, + }); + const methods = module.spec.methods.map(({ name, typeAnnotation }) => + methodOf(name, typeAnnotation, contextFor(name)), + ); + const events = (module.spec.eventEmitters ?? []).map(eventEmitter => + eventOf(eventEmitter, contextFor(eventEmitter.name)), + ); + const assertions = methods + .map( + method => ` static_assert( + facebook::react::bridging::getParameterCount(&T::${method.name}) == + ${method.parameterCount}, + "${method.message}");`, + ) + .join('\n'); + const registrations = [ + ...methods.map(method => ` ${method.registration}`), + ...events.map(event => ` ${event.registration}`), + ].join('\n'); + const emitters = events.map(event => event.emitter).join('\n\n'); + return `template +class ${hasteModuleName}Spec : public rngpui::TurboModuleBase { + public: + static constexpr std::string_view kModuleName = "${module.moduleName}"; + + protected: + explicit ${hasteModuleName}Spec( + std::shared_ptr jsInvoker) + : rngpui::TurboModuleBase( + std::string{kModuleName}, std::move(jsInvoker)) { +${assertions} +${registrations} + } +${emitters ? `\n${emitters}\n` : ''}};`; + }); + return `// Generated by @react-native-gpui/codegen; do not edit. +#pragma once + +#include "GpuiTurboModule.h" + +#include <${libraryName}JSI.h> + +#include +#include +#include +#include + +namespace facebook::react { + +${classes.join('\n\n')} + +} // namespace facebook::react +`; +} + +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + const inputs = process.argv.slice(2); + const outputPath = inputs.pop(); + const libraryName = inputs.pop(); + if (!outputPath || !libraryName || inputs.length === 0) { + throw new Error( + 'usage: generate-cxx-spec ... ', + ); + } + const require = createRequire(import.meta.url); + const { + combineSchemasInFileList, + } = require('@react-native/codegen/lib/cli/combine/combine-js-to-schema'); + const specPaths = Object.fromEntries( + inputs.map(specPath => [ + path.basename(specPath).replace(/\.[^.]+$/u, ''), + specPath, + ]), + ); + const schema = combineSchemasInFileList(inputs, 'desktop', null, libraryName); + const generated = generateCxxSpec({ libraryName, schema, specPaths }); + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + // Keep the mtime stable so C++ that includes it is not recompiled. + if ( + !fs.existsSync(outputPath) || + fs.readFileSync(outputPath, 'utf8') !== generated + ) { + fs.writeFileSync(outputPath, generated); + } +} diff --git a/vendor/react-native-gpui/packages/codegen/src/generate-cxx-spec.test.js b/vendor/react-native-gpui/packages/codegen/src/generate-cxx-spec.test.js new file mode 100644 index 0000000000..7a27b5b8be --- /dev/null +++ b/vendor/react-native-gpui/packages/codegen/src/generate-cxx-spec.test.js @@ -0,0 +1,193 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { expect, test } from 'vitest'; +import { generateCxxSpec } from './generate-cxx-spec.js'; + +const require = createRequire(import.meta.url); + +const spec = `import type { CodegenTypes, TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + greet(name: string): string; + record(value: string): void; + greetLater(name: string): Promise; + lookup(key: string, callback: (value: string, found: boolean) => void): void; + readonly onProgress: CodegenTypes.EventEmitter; +} + +export default TurboModuleRegistry.getEnforcing('Probe'); +`; + +/** Runs the real TS parser over `source` and returns the generated header. */ +function generateFor(source) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-gpui-spec-')); + const specPath = path.join(directory, 'NativeProbe.ts'); + fs.writeFileSync(specPath, source); + const { + combineSchemasInFileList, + } = require('@react-native/codegen/lib/cli/combine/combine-js-to-schema'); + const schema = combineSchemasInFileList( + [specPath], + 'desktop', + null, + 'ProbeSpec', + ); + return generateCxxSpec({ + libraryName: 'ProbeSpec', + schema, + specPaths: { NativeProbe: specPath }, + }); +} + +test('generates a spec class per module with one registration per method', () => { + const header = generateFor(spec); + expect(header).toContain('#include "GpuiTurboModule.h"'); + expect(header).toContain('#include '); + expect(header).toContain( + 'class NativeProbeSpec : public rngpui::TurboModuleBase', + ); + expect(header).toContain( + 'static constexpr std::string_view kModuleName = "Probe";', + ); + expect(header).toContain('this->template valueMethod<&T::greet>("greet");'); + expect(header).toContain('this->template voidMethod<&T::record>("record");'); + expect(header).toContain( + 'this->template promiseMethod<&T::greetLater>("greetLater");', + ); +}); + +test('asserts the parameter count with the expected signature in words', () => { + const header = generateFor(spec); + expect(header).toContain( + '"greet must be declared as std::string greet(jsi::Runtime& rt, std::string name)"', + ); + expect(header).toContain( + '"record must be declared as void record(std::string value); with kThread = JS it takes jsi::Runtime& rt first"', + ); + expect(header).toContain( + '"greetLater must be declared as void greetLater(std::string name, rngpui::Promise); with kThread = JS it takes jsi::Runtime& rt first"', + ); + expect(header).toContain( + 'getParameterCount(&T::greet) ==\n 1 + 1,', + ); + expect(header).toContain( + 'getParameterCount(&T::greetLater) ==\n (T::kThread == rngpui::ModuleThread::JS ? 1 : 0) + 1 + 1,', + ); +}); + +test('converts a callback parameter to rngpui::Callback', () => { + const header = generateFor(spec); + expect(header).toContain('rngpui::Callback callback'); + expect(header).toContain( + 'void lookup(std::string key, rngpui::Callback callback)', + ); +}); + +test('registers a typed event and generates its emitter', () => { + const header = generateFor(spec); + expect(header).toContain( + 'this->template addEvent("onProgress");', + ); + expect(header).toContain('void emitOnProgress(V value)'); + expect(header).toContain( + 'facebook::react::bridging::supportsToJs', + ); + expect(header).toContain('this->emitEvent("onProgress", std::move(value));'); +}); + +test('rejects a value-returning method with a callback parameter', () => { + expect(() => + generateFor( + spec.replace( + 'lookup(key: string, callback: (value: string, found: boolean) => void): void;', + 'lookup(key: string, callback: (value: string, found: boolean) => void): string;', + ), + ), + ).toThrow( + /lookup: a method that takes a callback must return void or a Promise/u, + ); +}); + +test('rejects getConstants declared with a Promise', () => { + expect(() => + generateFor( + spec.replace( + 'greet(name: string): string;', + 'getConstants(): Promise;', + ), + ), + ).toThrow(/getConstants: getConstants cannot return a Promise/u); +}); + +test('describes an optional parameter as std::optional', () => { + const header = generateFor( + spec.replace( + 'record(value: string): void;', + 'record(value: string, tag?: string): void;', + ), + ); + expect(header).toContain( + 'record must be declared as void record(std::string value, std::optional tag)', + ); +}); + +test('maps an ArrayBuffer to bytes it owns, in arguments and results', () => { + const header = generateFor( + spec + .replace( + 'record(value: string): void;', + 'record(value: ArrayBuffer): void;', + ) + .replace( + 'readonly onProgress: CodegenTypes.EventEmitter;', + 'readonly onProgress: CodegenTypes.EventEmitter;', + ) + .replace( + 'greet(name: string): string;', + 'greet(name: string): ArrayBuffer;', + ), + ); + expect(header).toContain('void record(std::vector value);'); + expect(header).toContain('std::vector greet('); + expect(header).toContain('void emitOnProgress(V value)'); + expect(header).toContain('supportsToJs>'); +}); + +test('names the method or event emitter a failing type came from', () => { + expect(() => + generateFor( + spec.replace( + 'record(value: string): void;', + "record(value: 'a' | 1): void;", + ), + ), + ).toThrow( + /NativeProbe\.ts: record: unsupported spec type UnionTypeAnnotation with mixed member types/u, + ); +}); + +test('generates a payload-less event emitter and a Promise method', () => { + const header = generateFor( + spec + .replace( + 'readonly onProgress: CodegenTypes.EventEmitter;', + 'readonly onDone: CodegenTypes.EventEmitter;', + ) + .replace( + 'greet(name: string): string;', + 'greet(name: string): string;\n finish(): Promise;', + ), + ); + expect(header).toContain('this->template addEvent<>("onDone");'); + expect(header).toContain('void emitOnDone() {'); + expect(header).toContain('this->emitEvent("onDone");'); + expect(header).toContain( + '"finish must be declared as void finish(rngpui::Promise<>)', + ); + expect(header).toContain( + 'this->template promiseMethod<&T::finish>("finish");', + ); +}); diff --git a/vendor/react-native-gpui/packages/codegen/src/generate-host-component-cpp.js b/vendor/react-native-gpui/packages/codegen/src/generate-host-component-cpp.js new file mode 100644 index 0000000000..5c80ed5000 --- /dev/null +++ b/vendor/react-native-gpui/packages/codegen/src/generate-host-component-cpp.js @@ -0,0 +1,861 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { parseComponents, snakeCase } from './generate-rust-spec.js'; + +/// React Native's own C++ naming helpers, so an enum or a struct is named here +/// exactly as `Props.h` names it. +function loadNamingHelpers(hints) { + const failures = []; + for (const hint of hints) { + try { + const require = createRequire(hint); + const utils = require('@react-native/codegen/lib/generators/Utils.js'); + const cpp = require('@react-native/codegen/lib/generators/components/CppHelpers.js'); + return { + toSafeCppString: utils.toSafeCppString, + getEnumName: utils.getEnumName, + toIntEnumValueName: cpp.toIntEnumValueName, + }; + } catch (error) { + failures.push(`${hint}: ${error.message}`); + } + } + throw new Error( + `generate-host-component-cpp: cannot resolve @react-native/codegen; install it in the app.\n${failures.join('\n')}`, + ); +} + +/// The size and alignment of a Rust scalar as `repr(C)` lays it out. +const SCALAR_LAYOUT = { + i32: { size: 4, align: 4 }, + f32: { size: 4, align: 4 }, + f64: { size: 8, align: 8 }, + bool: { size: 1, align: 1 }, +}; + +/// The C++ type React Native's generator gives a Rust scalar. +const SCALAR_CPP = { + i32: 'int', + f32: 'rr::Float', + f64: 'double', + bool: 'bool', +}; + +/// The C ABI type of a scalar shim argument or return. +const SCALAR_ABI = { + i32: 'int32_t', + f32: 'float', + f64: 'double', + bool: 'bool', +}; + +const alignUp = (value, align) => Math.ceil(value / align) * align; + +/// `repr(C)` layout of a plain mirror: its size, alignment and field offsets. +function layoutOf(generator, mirror, cache = new Map()) { + if (cache.has(mirror.name)) return cache.get(mirror.name); + let offset = 0; + let align = 1; + const offsets = []; + for (const field of mirror.fields) { + let layout = SCALAR_LAYOUT[field.rustType]; + if (layout === undefined) { + const nested = generator.mirrors.find( + candidate => candidate.name === field.rustType, + ); + if (nested !== undefined) { + layout = layoutOf(generator, nested, cache); + } else if (generator.enums.some(e => e.name === field.rustType)) { + // `#[repr(i32)]`. + layout = { size: 4, align: 4 }; + } else { + throw new Error( + `generate-host-component-cpp: unknown mirror field type ${field.rustType}`, + ); + } + } + offset = alignUp(offset, layout.align); + offsets.push(offset); + offset += layout.size; + align = Math.max(align, layout.align); + } + const result = { size: alignUp(offset, align), align, offsets }; + cache.set(mirror.name, result); + return result; +} + +/// How expensive a prop is to compare, so the change mask checks cheap fields +/// first: a 10,000-point chart must not compare every point to answer "did the +/// title change". Bits are positional, so the order changes nothing observable. +function compareCost(shape) { + switch (shape.kind) { + case 'scalar': + case 'enum': + return 0; + case 'color': + case 'string': + return 1; + case 'mirror': + case 'view': + case 'image': + return 2; + case 'mixed': + return 4; + default: + return 3; + } +} + +const cppVar = value => value.replaceAll(/[^A-Za-z0-9_]/gu, '_'); + +/// One component's C++. +class ComponentCpp { + constructor(generator, flags, naming) { + this.generator = generator; + this.flags = flags; + this.naming = naming; + this.name = generator.name; + this.snake = snakeCase(generator.name); + this.propsType = `rr::${generator.name}Props`; + this.emitterType = `rr::${generator.name}EventEmitter`; + this.out = []; + } + + mirrorByName(name) { + return this.generator.mirrors.find(mirror => mirror.name === name); + } + + viewByName(name) { + return this.generator.views.find(view => view.name === name); + } + + cppMirror(name) { + return `rr::${this.mirrorByName(name).cppName}`; + } + + cppView(name) { + return `rr::${this.viewByName(name).cppName}`; + } + + enumCppName(declared) { + return this.naming.getEnumName(this.name, declared.propName); + } + + enumMemberCppName(declared, member) { + return declared.numeric + ? this.naming.toIntEnumValueName(declared.propName, member.text) + : this.naming.toSafeCppString(member.text); + } + + push(...lines) { + this.out.push(...lines); + } + + renderChecks() { + const { generator } = this; + this.push(`// ---- ${this.name}: layout checks`); + this.push( + `static_assert(std::is_base_of_v);`, + ); + const cache = new Map(); + for (const mirror of generator.mirrors) { + const type = `rr::${mirror.cppName}`; + const layout = layoutOf(generator, mirror, cache); + this.push(`static_assert(std::is_standard_layout_v<${type}>);`); + this.push(`static_assert(sizeof(${type}) == ${layout.size});`); + this.push(`static_assert(alignof(${type}) == ${layout.align});`); + mirror.fields.forEach((field, index) => { + this.push( + `static_assert(offsetof(${type}, ${field.jsName}) == ${layout.offsets[index]});`, + ); + const cppType = SCALAR_CPP[field.rustType]; + if (cppType !== undefined) { + this.push( + `static_assert(std::is_same_v);`, + ); + } + }); + } + for (const declared of generator.enums) { + const type = `rr::${this.enumCppName(declared)}`; + this.push(`static_assert(sizeof(${type}) == sizeof(int32_t));`); + for (const member of declared.members) { + this.push( + `static_assert(static_cast(${type}::${this.enumMemberCppName(declared, member)}) == ${member.value});`, + ); + } + } + for (const prop of generator.props) { + const cppType = + prop.shape.kind === 'scalar' ? SCALAR_CPP[prop.shape.rustType] : null; + if (cppType !== null) { + this.push( + `static_assert(std::is_same_v);`, + ); + } + } + for (const view of generator.views) { + for (const field of view.fields) { + const cppType = + field.shape.kind === 'scalar' + ? SCALAR_CPP[field.shape.rustType] + : null; + if (cppType !== null) { + this.push( + `static_assert(std::is_same_v);`, + ); + } + } + } + this.push(''); + } + + /// How to compare two values of a shape for the change mask. + equal(shape, a, b) { + switch (shape.kind) { + case 'image': + return `rn_eq_image(${a}, ${b})`; + case 'mirror': + return `eq_${cppVar(shape.name)}(${a}, ${b})`; + case 'view': + return `eq_${cppVar(shape.name)}(${a}, ${b})`; + case 'array-plain': + return `rn_eq_vec(${a}, ${b}, eq_${cppVar(shape.name)})`; + case 'array-view': + return `rn_eq_vec(${a}, ${b}, eq_${cppVar(shape.name)})`; + default: + return `${a} == ${b}`; + } + } + + /// A mirror's field is a scalar, an enum or another mirror. + mirrorFieldEqual(field, a, b) { + if (this.mirrorByName(field.rustType) !== undefined) { + return `eq_${cppVar(field.rustType)}(${a}, ${b})`; + } + return `${a} == ${b}`; + } + + renderEqualityDeclarations() { + for (const mirror of this.generator.mirrors) { + this.push( + `bool eq_${cppVar(mirror.name)}(const rr::${mirror.cppName}& a, const rr::${mirror.cppName}& b);`, + ); + } + for (const view of this.generator.views) { + this.push( + `bool eq_${cppVar(view.name)}(const rr::${view.cppName}& a, const rr::${view.cppName}& b);`, + ); + } + } + + renderEqualityDefinitions() { + for (const mirror of this.generator.mirrors) { + const terms = mirror.fields.map(field => + this.mirrorFieldEqual(field, `a.${field.jsName}`, `b.${field.jsName}`), + ); + this.push( + `bool eq_${cppVar(mirror.name)}(const rr::${mirror.cppName}& a, const rr::${mirror.cppName}& b) {`, + ` return ${terms.length === 0 ? 'true' : terms.join(' &&\n ')};`, + '}', + '', + ); + } + for (const view of this.generator.views) { + const terms = view.fields.map(field => + this.equal(field.shape, `a.${field.jsName}`, `b.${field.jsName}`), + ); + this.push( + `bool eq_${cppVar(view.name)}(const rr::${view.cppName}& a, const rr::${view.cppName}& b) {`, + ` return ${terms.length === 0 ? 'true' : terms.join(' &&\n ')};`, + '}', + '', + ); + } + } + + renderChanged() { + const { generator } = this; + this.push( + `void ${this.snake}_changed(const rr::Props& rawBefore, const rr::Props& rawAfter, uint64_t* words) {`, + ` const auto& before = static_cast(rawBefore);`, + ` const auto& after = static_cast(rawAfter);`, + ); + const ordered = [...generator.props].sort( + (left, right) => + compareCost(left.shape) - compareCost(right.shape) || + left.index - right.index, + ); + for (const prop of ordered) { + const same = this.equal( + prop.shape, + `before.${prop.jsName}`, + `after.${prop.jsName}`, + ); + const word = Math.floor(prop.index / 64); + const bit = prop.index % 64; + this.push( + ` if (!(${same})) {`, + ` words[${word}] |= uint64_t{1} << ${bit};`, + ' }', + ); + } + this.push('}', ''); + } + + /// The shims for one owner: the props handle, or a view's struct address. + renderAccessors({ shimPrefix, owner, fields }) { + for (const field of fields) { + const shim = `${shimPrefix}_${snakeCase(field.jsName)}`; + const value = `p.${field.jsName}`; + const head = (returns, extra = '') => + `${returns} ${shim}(const void* handle${extra}) {`; + this.push(`// ${field.jsName}`); + switch (field.shape.kind) { + case 'scalar': + this.push( + head(SCALAR_ABI[field.shape.rustType]), + owner, + ` return ${value};`, + '}', + ); + break; + case 'string': + this.push( + head('RnGpuiStr'), + owner, + ` return rn_str(${value});`, + '}', + ); + break; + case 'enum': + this.push( + head('int32_t'), + owner, + ` return static_cast(${value});`, + '}', + ); + break; + case 'color': + this.push( + head('RnGpuiOptU32'), + owner, + ` return rn_color(${value});`, + '}', + ); + break; + case 'image': + case 'view': + this.push(head('const void*'), owner, ` return &${value};`, '}'); + break; + case 'mixed': + this.push( + head('void', ', RnTextSink sink, void* context'), + owner, + ` rn_text(${value}, sink, context);`, + '}', + ); + break; + case 'mirror': + this.push( + head(this.cppMirror(field.shape.name)), + owner, + ` return ${value};`, + '}', + ); + break; + case 'array-scalar': + this.push( + `static_assert(sizeof(decltype(${value.replace('p.', `std::declval<${field.ownerType}>().`)})::value_type) == sizeof(${SCALAR_ABI[field.shape.rustType]}));`, + ); + this.push( + head('RnGpuiSlice'), + owner, + ` return rn_slice(${value});`, + '}', + ); + break; + case 'array-plain': + this.push( + head('RnGpuiSlice'), + owner, + ` return rn_slice(${value});`, + '}', + ); + break; + case 'array-string': + this.push( + `size_t ${shim}_len(const void* handle) {`, + owner, + ` return ${value}.size();`, + '}', + `RnGpuiStr ${shim}_at(const void* handle, size_t index) {`, + owner, + ` return rn_str(${value}[index]);`, + '}', + ); + break; + case 'array-view': + this.push( + `size_t ${shim}_len(const void* handle) {`, + owner, + ` return ${value}.size();`, + '}', + `const void* ${shim}_at(const void* handle, size_t index) {`, + owner, + ` return &${value}[index];`, + '}', + ); + break; + default: + throw new Error( + `generate-host-component-cpp: unsupported accessor shape ${field.shape.kind}`, + ); + } + this.push(''); + } + } + + /// The shim parameters and the statements that fill the event struct, walking + /// the payload leaf by leaf in the order the Rust half flattens it. + flatten(declared, valueExpression, prefix, params, statements) { + for (const field of declared.fields) { + const argument = cppVar( + prefix === '' ? field.name : `${prefix}_${field.name}`, + ); + const target = `${valueExpression}.${field.jsName}`; + switch (field.shape.kind) { + case 'scalar': + params.push(`${SCALAR_ABI[field.shape.rustType]} ${argument}`); + statements.push(`${target} = ${argument};`); + break; + case 'enum': + params.push(`int32_t ${argument}`); + statements.push( + `${target} = static_cast(${argument});`, + ); + break; + case 'string': + params.push( + `const uint8_t* ${argument}_data`, + `size_t ${argument}_len`, + ); + statements.push( + `${target}.assign(reinterpret_cast(${argument}_data), ${argument}_len);`, + ); + break; + case 'array-scalar': + params.push( + `const ${SCALAR_ABI[field.shape.rustType]}* ${argument}_data`, + `size_t ${argument}_len`, + ); + statements.push( + `${target}.assign(${argument}_data, ${argument}_data + ${argument}_len);`, + ); + break; + case 'array-string': + params.push( + `const RnGpuiStr* ${argument}_data`, + `size_t ${argument}_len`, + ); + statements.push( + `${target}.clear();`, + `for (size_t i = 0; i < ${argument}_len; ++i) {`, + ` ${target}.emplace_back(reinterpret_cast(${argument}_data[i].data), ${argument}_data[i].len);`, + '}', + ); + break; + case 'object': { + const nested = this.generator.payloads.find( + candidate => candidate.name === field.shape.typeName, + ); + this.flatten(nested, target, argument, params, statements); + break; + } + default: + throw new Error( + `generate-host-component-cpp: unsupported event payload shape ${field.shape.kind}`, + ); + } + } + } + + renderEvents() { + for (const event of this.generator.events) { + const shim = `rn_gpui_${this.snake}_event_${snakeCase(event.name)}`; + this.push(`// ${event.jsName}`); + if (event.payload === null) { + this.push( + `void ${shim}(const void* emitter) {`, + ` rngpui::hostEmitter<${this.emitterType}>(emitter).${event.jsName}({});`, + '}', + '', + ); + continue; + } + const params = []; + const statements = []; + this.flatten(event.payload, 'value', '', params, statements); + this.push( + `void ${shim}(${['const void* emitter', ...params].join(', ')}) {`, + ` ${this.emitterType}::${event.payload.name} value{};`, + ...statements.map(statement => ` ${statement}`), + ` rngpui::hostEmitter<${this.emitterType}>(emitter).${event.jsName}(std::move(value));`, + '}', + '', + ); + } + } + + /// The check and the conversion of one command argument, as a + /// `HostCommandValue` appended to `values`. + commandArgument(parameter, position) { + const arg = `args[${position}]`; + const type = parameter.rustType; + const name = parameter.jsName; + const fail = what => + ` LOG(ERROR) << "[host-component] ${this.name}.${'${command}'}: argument '${name}' must be ${what}";\n return true;`; + switch (type) { + case 'i32': + case 'f32': + case 'f64': + return [ + ` if (!${arg}.isNumber()) {`, + fail('a number'), + ' }', + ` values.push_back(rn_number(${arg}.asDouble()));`, + ]; + case 'bool': + return [ + ` if (!${arg}.isBool()) {`, + fail('a boolean'), + ' }', + ` values.push_back(rn_bool(${arg}.asBool()));`, + ]; + case 'String': + return [ + ` if (!${arg}.isString()) {`, + fail('a string'), + ' }', + ` values.push_back(rn_string(${arg}.getString()));`, + ]; + case 'Vec': + case 'Vec': + case 'Vec': + return [ + ` if (!rn_all(${arg}, [](const folly::dynamic& v) { return v.isNumber(); })) {`, + fail('an array of numbers'), + ' }', + ` values.push_back(rn_numbers(${arg}));`, + ]; + case 'Vec': + return [ + ` if (!rn_all(${arg}, [](const folly::dynamic& v) { return v.isBool(); })) {`, + fail('an array of booleans'), + ' }', + ` values.push_back(rn_bools(${arg}));`, + ]; + case 'Vec': + return [ + ` if (!rn_all(${arg}, [](const folly::dynamic& v) { return v.isString(); })) {`, + fail('an array of strings'), + ' }', + ` values.push_back(rn_strings(${arg}));`, + ]; + default: + throw new Error( + `generate-host-component-cpp: unsupported command argument ${type}`, + ); + } + } + + renderCommand() { + const { commands } = this.generator; + this.push( + `bool ${this.snake}_command(int32_t tag, const std::string& command, const folly::dynamic& args) {`, + ); + commands.forEach((command, commandIndex) => { + this.push(` if (command == ${JSON.stringify(command.jsName)}) {`); + this.push( + ` if (!args.isArray() || args.size() != ${command.params.length}) {`, + ` LOG(ERROR) << "[host-component] ${this.name}.${command.jsName} takes ${command.params.length} argument(s)";`, + ' return true;', + ' }', + ' std::vector values;', + ` values.reserve(${command.params.length});`, + ); + command.params.forEach((parameter, position) => { + const lines = this.commandArgument(parameter, position) + .map(line => line.replace('${command}', command.jsName)) + .map(line => ` ${line}`); + this.push(...lines); + }); + this.push( + ` rngpui::dispatchHostCommand(tag, ${commandIndex}, std::move(values));`, + ' return true;', + ' }', + ); + }); + this.push(' (void)tag;', ' (void)args;', ' return false;', '}', ''); + } + + render() { + const { generator, flags } = this; + const nameVariable = `k${this.name}Name`; + const shadow = `${this.name}ShadowNode`; + const descriptor = `${this.name}Descriptor`; + this.push(`// ================= ${this.name} =================`, ''); + this.push('namespace {', ''); + this.push( + `inline constexpr char ${nameVariable}[] = ${JSON.stringify(this.name)};`, + ); + if (flags.container) { + this.push( + `using ${shadow} = rngpui::GpuiHostContainerShadowNode<${nameVariable}, ${this.propsType}, ${this.emitterType}>;`, + `using ${descriptor} = rngpui::GpuiHostContainerDescriptor<${shadow}>;`, + ); + } else { + this.push( + `using ${shadow} = rngpui::GpuiHostLeafShadowNode<${nameVariable}, ${this.propsType}, ${this.emitterType}, ${flags.measured ? 'true' : 'false'}>;`, + `using ${descriptor} = rr::ConcreteComponentDescriptor<${shadow}>;`, + ); + } + this.push(''); + this.renderChecks(); + this.renderEqualityDeclarations(); + this.push(''); + this.renderEqualityDefinitions(); + this.renderChanged(); + this.renderCommand(); + this.push(`} // namespace`, ''); + + this.push('extern "C" {', ''); + const propsOwner = ` const auto& p = rngpui::hostProps<${this.propsType}>(handle);`; + this.renderAccessors({ + shimPrefix: `rn_gpui_${this.snake}_props`, + owner: propsOwner, + fields: generator.props.map(prop => ({ + ...prop, + ownerType: this.propsType, + })), + }); + for (const view of generator.views) { + this.renderAccessors({ + shimPrefix: `rn_gpui_${snakeCase(view.name)}`, + owner: ` const auto& p = *static_cast(handle);`, + fields: view.fields.map(field => ({ + ...field, + ownerType: `rr::${view.cppName}`, + })), + }); + } + this.renderEvents(); + this.push('} // extern "C"', ''); + } + + registration(index) { + const { flags } = this; + const nameVariable = `k${this.name}Name`; + return [ + ' {', + ' rngpui::HostComponentEntry entry;', + ` entry.index = ${index};`, + ` entry.name = ${nameVariable};`, + ` entry.handle = ${this.name}ShadowNode::Handle();`, + ` entry.measured = ${flags.measured};`, + ` entry.container = ${flags.container};`, + ` entry.nativeSurface = ${flags.nativeSurface};`, + ` entry.provider = &rr::concreteComponentDescriptorProvider<${this.name}Descriptor>;`, + ` entry.changed = &${this.snake}_changed;`, + ` entry.changeWords = ${this.generator.changeWords};`, + ` entry.command = &${this.snake}_command;`, + ' rngpui::registerHostComponentEntry(entry);', + ' }', + ]; + } +} + +const PROLOGUE = `#include "GpuiHostComponentsCpp.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace rr = facebook::react; + +namespace { +// Borrowed views of C++ values, as the ABI structs core declares carry them. +inline RnGpuiStr rn_str(const std::string& text) { + return RnGpuiStr{reinterpret_cast(text.data()), text.size()}; +} + +template +inline RnGpuiSlice rn_slice(const std::vector& values) { + return RnGpuiSlice{values.data(), values.size()}; +} + +inline RnGpuiOptU32 rn_color(const rr::SharedColor& color) { + // React Native's unset color is zero. + return RnGpuiOptU32{static_cast(*color), static_cast(color)}; +} + +// A \`folly::dynamic\` has no layout to lend out: it is serialised once and the +// bytes are handed to a callback that copies them. +using RnTextSink = void (*)(void* context, const uint8_t* data, size_t len); + +inline void rn_text(const folly::dynamic& value, RnTextSink sink, void* context) { + const std::string text = folly::toJson(value); + sink(context, reinterpret_cast(text.data()), text.size()); +} + +inline bool rn_eq_image(const rr::ImageSource& a, const rr::ImageSource& b) { + return a.type == b.type && a.uri == b.uri && a.bundle == b.bundle && + a.scale == b.scale && a.size.width == b.size.width && + a.size.height == b.size.height && a.body == b.body && + a.method == b.method; +} + +template +inline bool rn_eq_vec(const std::vector& a, const std::vector& b, Equal equal) { + if (a.size() != b.size()) { + return false; + } + for (size_t i = 0; i < a.size(); ++i) { + if (!equal(a[i], b[i])) { + return false; + } + } + return true; +} + +// Command arguments, validated by the generated dispatcher below. +inline rngpui::HostCommandValue rn_number(double value) { + rngpui::HostCommandValue out; + out.kind = rngpui::HostCommandValue::Number; + out.number = value; + return out; +} + +inline rngpui::HostCommandValue rn_bool(bool value) { + rngpui::HostCommandValue out; + out.kind = rngpui::HostCommandValue::Bool; + out.number = value ? 1 : 0; + return out; +} + +inline rngpui::HostCommandValue rn_string(const std::string& value) { + rngpui::HostCommandValue out; + out.kind = rngpui::HostCommandValue::String; + out.text = value; + return out; +} + +template +inline bool rn_all(const folly::dynamic& array, Predicate predicate) { + if (!array.isArray()) { + return false; + } + for (const auto& item : array) { + if (!predicate(item)) { + return false; + } + } + return true; +} + +inline rngpui::HostCommandValue rn_numbers(const folly::dynamic& array) { + rngpui::HostCommandValue out; + out.kind = rngpui::HostCommandValue::Numbers; + for (const auto& item : array) { + out.numbers.push_back(item.asDouble()); + } + return out; +} + +inline rngpui::HostCommandValue rn_bools(const folly::dynamic& array) { + rngpui::HostCommandValue out; + out.kind = rngpui::HostCommandValue::Bools; + for (const auto& item : array) { + out.numbers.push_back(item.asBool() ? 1 : 0); + } + return out; +} + +inline rngpui::HostCommandValue rn_strings(const folly::dynamic& array) { + rngpui::HostCommandValue out; + out.kind = rngpui::HostCommandValue::Strings; + for (const auto& item : array) { + out.texts.push_back(item.getString()); + } + return out; +} +} // namespace +`; + +/// The C++ source for every component spec in \`specPaths\`. +export function generateHostComponentCpp({ + specPaths, + libraryName, + components, +}) { + const generators = parseComponents({ specPaths }); + if (generators.length === 0) { + return ''; + } + const naming = loadNamingHelpers([ + ...specPaths.map(specPath => path.resolve(specPath)), + path.join(process.cwd(), 'package.json'), + import.meta.url, + ]); + const parts = []; + const registrations = []; + for (const generator of generators) { + const declared = components[generator.name]; + if (declared === undefined) { + throw new Error( + `${generator.specPath}: ${generator.name} is not declared under platforms.desktop.hostComponents in react-native.config.js`, + ); + } + if (generator.component.interfaceOnly !== true) { + throw new Error( + `${generator.specPath}: ${generator.name} must be created with codegenNativeComponent(..., { interfaceOnly: true }); core supplies the shadow node and the descriptor, so React Native must not generate its own`, + ); + } + if (declared.measured && declared.container) { + throw new Error( + `${generator.name}: a host component cannot be both measured and a container; Fabric's MeasurableYogaNode requires a leaf, and a leaf cannot hold children`, + ); + } + const cpp = new ComponentCpp(generator, declared, naming); + cpp.render(); + parts.push(cpp.out.join('\n')); + registrations.push(cpp.registration(declared.index).join('\n')); + } + const identifier = cppVar(libraryName); + return `${[ + '// Generated by @react-native-gpui/codegen from TypeScript specs; do not edit.', + '//', + '// The C++ half of the host component API: it implements the shims the generated', + "// Rust declares, reading React Native's own generated props in place, and", + "// registers each component with core's registry.", + '', + `#include `, + `#include `, + '', + PROLOGUE, + ...parts, + `extern "C" void rn_gpui_register_host_components_${identifier}() {`, + ...registrations, + '}', + ].join('\n')}\n`; +} diff --git a/vendor/react-native-gpui/packages/codegen/src/generate-host-component-cpp.test.js b/vendor/react-native-gpui/packages/codegen/src/generate-host-component-cpp.test.js new file mode 100644 index 0000000000..5243949353 --- /dev/null +++ b/vendor/react-native-gpui/packages/codegen/src/generate-host-component-cpp.test.js @@ -0,0 +1,437 @@ +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { describe, expect, test } from 'vitest'; +import { generateHostComponentCpp } from './generate-host-component-cpp.js'; +import { generateRustSpec } from './generate-rust-spec.js'; +import { KITCHEN_SINK_SPEC } from './kitchen-sink-spec.js'; + +const FLAGS = { + index: 0, + measured: false, + container: false, + nativeSurface: false, +}; + +const writeSpec = (name, source) => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), 'host-component-cpp-'), + ); + const file = path.join(directory, `${name}NativeComponent.ts`); + fs.writeFileSync(file, source); + return file; +}; + +const specFile = writeSpec('FixtureChart', KITCHEN_SINK_SPEC); + +const cpp = generateHostComponentCpp({ + specPaths: [specFile], + libraryName: 'KitchenSinkSpec', + components: { FixtureChart: FLAGS }, +}); +const rust = generateRustSpec({ specPaths: [specFile] }); + +/// The names of the `extern "C"` shims the generated Rust declares. +const declaredShims = () => { + const block = rust.slice(rust.lastIndexOf('unsafe extern "C" {')); + return [...block.matchAll(/^\s+fn (rn_gpui_\w+)\(/gmu)].map( + match => match[1], + ); +}; + +describe('the C++ half implements the shims the Rust half declares', () => { + test('every declared shim has a definition of the same name', () => { + const shims = declaredShims(); + expect(shims.length).toBeGreaterThan(20); + const missing = shims.filter( + shim => !new RegExp(`\\b${shim}\\(`, 'u').test(cpp), + ); + expect( + missing.filter(shim => !shim.startsWith('rn_gpui_image_source_')), + ).toEqual([]); + }); + + test('no shim is defined that Rust does not declare', () => { + const declared = new Set(declaredShims()); + const defined = [...cpp.matchAll(/^\S.*\b(rn_gpui_\w+)\(/gmu)].map( + match => match[1], + ); + expect( + defined.filter( + shim => + !declared.has(shim) && + !shim.startsWith('rn_gpui_register_host_components_'), + ), + ).toEqual([]); + }); + + test('the Rust half has no shim for the change mask: C++ computes it', () => { + expect(rust).not.toContain('fn rn_gpui_fixture_chart_changed('); + expect(cpp).toContain('void fixture_chart_changed('); + }); +}); + +describe('the generated file registers the component with core', () => { + test('carries its index, flags and entry points', () => { + const declared = generateHostComponentCpp({ + specPaths: [specFile], + libraryName: 'KitchenSinkSpec', + components: { + FixtureChart: { + index: 7, + measured: false, + container: true, + nativeSurface: true, + }, + }, + }); + expect(declared).toContain('entry.index = 7;'); + expect(declared).toContain('entry.container = true;'); + expect(declared).toContain('entry.nativeSurface = true;'); + expect(declared).toContain('entry.measured = false;'); + expect(declared).toContain('entry.changeWords = 1;'); + expect(declared).toContain( + 'extern "C" void rn_gpui_register_host_components_KitchenSinkSpec() {', + ); + }); + + test("instantiates core's templates over the types React Native generated", () => { + expect(cpp).toContain( + 'rngpui::GpuiHostLeafShadowNode', + ); + expect(cpp).toContain( + 'rr::ConcreteComponentDescriptor', + ); + expect(cpp).toContain( + 'entry.provider = &rr::concreteComponentDescriptorProvider;', + ); + }); + + test('a measured component is a measured leaf, a container uses its own descriptor', () => { + const measured = generateHostComponentCpp({ + specPaths: [specFile], + libraryName: 'KitchenSinkSpec', + components: { FixtureChart: { ...FLAGS, measured: true } }, + }); + expect(measured).toContain('rr::FixtureChartEventEmitter, true>'); + const container = generateHostComponentCpp({ + specPaths: [specFile], + libraryName: 'KitchenSinkSpec', + components: { FixtureChart: { ...FLAGS, container: true } }, + }); + expect(container).toContain( + 'rngpui::GpuiHostContainerShadowNode', + ); + expect(container).toContain( + 'rngpui::GpuiHostContainerDescriptor', + ); + }); +}); + +describe('the change mask', () => { + test('sets one bit per changed prop, in the order the Rust mask names them', () => { + expect(cpp).toContain('words[0] |= uint64_t{1} << 0;'); + expect(cpp).toContain('words[0] |= uint64_t{1} << 14;'); + }); + + test('compares cheap fields before arrays and dynamic values', () => { + const body = cpp.slice(cpp.indexOf('void fixture_chart_changed(')); + const scalar = body.indexOf('before.lineWidth == after.lineWidth'); + const array = body.indexOf('rn_eq_vec(before.points'); + const dynamic = body.indexOf('before.extra == after.extra'); + expect(scalar).toBeGreaterThan(-1); + expect(array).toBeGreaterThan(scalar); + expect(dynamic).toBeGreaterThan(array); + }); + + test('compares a struct field by field, never by memcmp over padding', () => { + expect(cpp).not.toContain('memcmp'); + expect(cpp).toContain('bool eq_FixtureChartPoints('); + }); + + test('spans a word per 64 props', () => { + const props = Array.from( + { length: 130 }, + (_, index) => ` prop${index}?: Int32;`, + ); + const file = writeSpec( + 'Many', + `import { codegenNativeComponent, type ViewProps } from 'react-native'; +export interface NativeProps extends ViewProps { +${props.join('\n')} +} +export default codegenNativeComponent('Many', { interfaceOnly: true }); +`, + ); + const many = generateHostComponentCpp({ + specPaths: [file], + libraryName: 'ManySpec', + components: { Many: FLAGS }, + }); + expect(many).toContain('entry.changeWords = 3;'); + expect(many).toContain('words[2] |= uint64_t{1} << 1;'); + }); +}); + +describe('commands and events', () => { + test('validate arguments against the spec and dispatch by index', () => { + expect(cpp).toContain('bool fixture_chart_command('); + expect(cpp).toContain('if (command == "selectPoint")'); + expect(cpp).toContain( + 'rngpui::dispatchHostCommand(tag, 0, std::move(values));', + ); + expect(cpp).toContain( + 'rngpui::dispatchHostCommand(tag, 1, std::move(values));', + ); + expect(cpp).toContain( + 'rngpui::dispatchHostCommand(tag, 2, std::move(values));', + ); + expect(cpp).toContain('.isNumber()'); + expect(cpp).toContain('rn_numbers(args[0])'); + expect(cpp).toContain('.isString()'); + expect(cpp).toContain( + "FixtureChart.selectPoint: argument 'index' must be a number", + ); + }); + + test('fill the generated event struct from the flattened leaves', () => { + expect(cpp).toContain( + 'rr::FixtureChartEventEmitter::OnPointSelected value{};', + ); + expect(cpp).toContain('value.index = index;'); + expect(cpp).toContain('.onPointSelected(std::move(value));'); + expect(cpp).toContain('value.nested.flag = nested_flag;'); + expect(cpp).toContain('value.nested.names.emplace_back('); + expect(cpp).toContain( + 'void rn_gpui_fixture_chart_event_done(const void* emitter) {', + ); + }); +}); + +describe('the generator rejects a component core cannot host', () => { + test('one that is not declared under hostComponents', () => { + expect(() => + generateHostComponentCpp({ + specPaths: [specFile], + libraryName: 'KitchenSinkSpec', + components: {}, + }), + ).toThrow( + /FixtureChart is not declared under platforms\.desktop\.hostComponents/u, + ); + }); + + test('one that is not interfaceOnly: React Native would generate a second descriptor', () => { + const file = writeSpec( + 'Plain', + KITCHEN_SINK_SPEC.replace( + /\{\s*interfaceOnly: true,?\s*\}/u, + '{ paperComponentName: "FixtureChart" }', + ), + ); + expect(() => + generateHostComponentCpp({ + specPaths: [file], + libraryName: 'KitchenSinkSpec', + components: { FixtureChart: FLAGS }, + }), + ).toThrow( + /must be created with codegenNativeComponent\(\.\.\., \{ interfaceOnly: true \}\)/u, + ); + }); + + test('one that is both measured and a container', () => { + expect(() => + generateHostComponentCpp({ + specPaths: [specFile], + libraryName: 'KitchenSinkSpec', + components: { + FixtureChart: { ...FLAGS, measured: true, container: true }, + }, + }), + ).toThrow(/cannot be both measured and a container/u); + }); +}); + +/// The compile flags of core's own C++, from a CMake tree that was built here. +/// Absent on a machine that never built the app, where the compile test skips. +const buildTree = path.resolve( + path.dirname(new URL(import.meta.url).pathname), + '../../../target/rn-gpui-cmake/aarch64-apple-darwin/Debug/build', +); +const flagsFile = path.join( + buildTree, + 'CMakeFiles/rn_gpui_core.dir/flags.make', +); +const canCompile = fs.existsSync(flagsFile) && fs.existsSync('/usr/bin/c++'); + +const flag = name => { + const line = fs + .readFileSync(flagsFile, 'utf8') + .split('\n') + .find(candidate => candidate.startsWith(`${name} =`)); + return line === undefined ? '' : line.slice(name.length + 3); +}; + +/// The newest cxx bridge header cargo generated, which declares the shared +/// structs core's headers include. +const bridgeInclude = () => { + const builds = path.resolve(buildTree, '../../../../debug/build'); + const candidates = fs + .readdirSync(builds) + .filter(entry => entry.startsWith('rn-gpui-sys-')) + .map(entry => path.join(builds, entry, 'out/cxxbridge/include')) + .filter(directory => + fs.existsSync(path.join(directory, 'rn-gpui-sys/src/lib.rs.h')), + ) + .sort( + (left, right) => + fs.statSync(path.join(right, 'rn-gpui-sys/src/lib.rs.h')).mtimeMs - + fs.statSync(path.join(left, 'rn-gpui-sys/src/lib.rs.h')).mtimeMs, + ); + return candidates[0]; +}; + +describe.skipIf(!canCompile)( + 'the generated file compiles against the real headers', + () => { + const root = path.resolve( + path.dirname(new URL(import.meta.url).pathname), + '../../..', + ); + + /// Generates React Native's own `Props.h` and `EventEmitters.h` for `file` + /// into a fresh include tree, and returns the include directory. + const reactNativeHeaders = (file, libraryName) => { + const require = createRequire(path.join(root, 'package.json')); + const schema = + require('@react-native/codegen/lib/cli/combine/combine-js-to-schema').combineSchemasInFileList( + [file], + 'desktop', + null, + libraryName, + ); + const include = fs.mkdtempSync( + path.join(os.tmpdir(), 'host-component-include-'), + ); + const staging = fs.mkdtempSync( + path.join(os.tmpdir(), 'host-component-rn-'), + ); + const generated = + require('@react-native/codegen/lib/generators/RNCodegen.js').generate( + { + libraryName, + schema, + outputDirectory: staging, + packageName: 'test', + assumeNonnull: false, + }, + { + generators: [ + 'descriptors', + 'events', + 'props', + 'states', + 'shadow-nodes', + ], + }, + ); + expect(generated).toBe(true); + const target = path.join( + include, + 'react/renderer/components', + libraryName, + ); + fs.mkdirSync(target, { recursive: true }); + for (const header of ['Props.h', 'EventEmitters.h']) { + fs.copyFileSync(path.join(staging, header), path.join(target, header)); + } + return include; + }; + + const compile = (source, include) => { + const file = path.join( + fs.mkdtempSync(path.join(os.tmpdir(), 'host-component-cc-')), + 'generated.cpp', + ); + fs.writeFileSync(file, source); + const bridge = bridgeInclude(); + const command = [ + '/usr/bin/c++', + flag('CXX_DEFINES'), + flag('CXX_FLAGS'), + `-I${include}`, + `-I${bridge}`, + `-I${path.join(bridge, '../crate')}`, + flag('CXX_INCLUDES'), + '-fsyntax-only', + file, + ].join(' '); + return spawnSync('/bin/sh', ['-c', command], { + cwd: buildTree, + encoding: 'utf8', + }); + }; + + test('every layout, offset and enum value matches the real types', () => { + const include = reactNativeHeaders(specFile, 'KitchenSinkSpec'); + const result = compile(cpp, include); + expect(result.stderr).toBe(''); + expect(result.status).toBe(0); + }, 120_000); + + test('a wrong layout breaks the build instead of corrupting a prop', () => { + const include = reactNativeHeaders(specFile, 'KitchenSinkSpec'); + const broken = cpp.replace( + 'offsetof(rr::FixtureChartPointsStruct, y) == 8', + 'offsetof(rr::FixtureChartPointsStruct, y) == 16', + ); + expect(broken).not.toBe(cpp); + const result = compile(broken, include); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain('static assertion failed'); + }, 120_000); + + test('a measured leaf, a container and a 130 prop component compile too', () => { + const include = reactNativeHeaders(specFile, 'KitchenSinkSpec'); + for (const flags of [ + { ...FLAGS, measured: true }, + { ...FLAGS, container: true, nativeSurface: true }, + ]) { + const source = generateHostComponentCpp({ + specPaths: [specFile], + libraryName: 'KitchenSinkSpec', + components: { FixtureChart: flags }, + }); + const result = compile(source, include); + expect(result.stderr).toBe(''); + } + + const props = Array.from( + { length: 130 }, + (_, index) => ` prop${index}?: Int32;`, + ); + const many = writeSpec( + 'Many', + `import { codegenNativeComponent, type ViewProps } from 'react-native'; +export interface NativeProps extends ViewProps { +${props.join('\n')} +} +export default codegenNativeComponent('Many', { interfaceOnly: true }); +`, + ); + const manyInclude = reactNativeHeaders(many, 'ManySpec'); + const result = compile( + generateHostComponentCpp({ + specPaths: [many], + libraryName: 'ManySpec', + components: { Many: FLAGS }, + }), + manyInclude, + ); + expect(result.stderr).toBe(''); + }, 240_000); + }, +); diff --git a/vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.component.test.js b/vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.component.test.js new file mode 100644 index 0000000000..ed18cd94d8 --- /dev/null +++ b/vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.component.test.js @@ -0,0 +1,444 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { describe, expect, test } from 'vitest'; +import { generateRustSpec } from './generate-rust-spec.js'; +import { KITCHEN_SINK_SPEC as FIXTURE } from './kitchen-sink-spec.js'; + +/// Generates from a spec body, written to a temporary file because the parser +/// reads the spec from disk. +const generateFrom = spec => { + const directory = fs.mkdtempSync( + path.join(os.tmpdir(), 'rust-component-spec-'), + ); + const file = path.join(directory, 'FixtureComponentSpec.ts'); + fs.writeFileSync(file, spec); + return generateRustSpec({ specPaths: [file] }); +}; + +/// A spec with `props` as its only props, for one table row at a time. +const withProps = props => + `import { + codegenNativeComponent, + type CodegenTypes, + type ViewProps, +} from 'react-native'; + +export interface NativeProps extends ViewProps { +${props} +} + +const Temp: any = null; + +export default codegenNativeComponent('Temp', { interfaceOnly: true }); +`; + +const header = generateFrom(FIXTURE); + +const countOf = (text, needle) => text.split(needle).length - 1; + +describe('a component spec becomes the generated API', () => { + test('renders the seven artifacts 5.1 lists', () => { + expect(header).toContain('pub struct FixtureChartProps {'); + expect(header).toContain('pub struct FixtureChartChanged {'); + expect(header).toContain('pub struct FixtureChartEvents<'); + expect(header).toContain('pub trait FixtureChartSpec: Sized +'); + expect(header).toContain('pub struct FixtureChartContext {'); + expect(header).toContain( + 'pub const fn fixture_chart_component() -> FixtureChartComponent {', + ); + // The `#[repr(C)]` mirror the props and the C++ file agree on. + expect(header).toContain( + '#[repr(C)]\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct FixtureChartPoints {', + ); + }); + + test('generates the trait of 5.1 exactly', () => { + expect(header).toContain( + 'fn create(props: &FixtureChartProps, cx: &mut FixtureChartContext) -> Self;', + ); + expect(header).toContain( + 'fn update(\n &mut self,\n props: &FixtureChartProps,\n changed: FixtureChartChanged,\n cx: &mut FixtureChartContext,\n );', + ); + expect(header).toContain( + 'fn render(\n &mut self,\n props: &FixtureChartProps,\n window: &mut Window,\n cx: &mut FixtureChartContext,\n ) -> AnyElement;', + ); + }); + + test('gives every command a typed method, in snake_case', () => { + expect(header).toContain( + 'fn select_point(&mut self, index: i32, cx: &mut FixtureChartContext);', + ); + expect(header).toContain( + 'fn report_series(&mut self, values: Vec, cx: &mut FixtureChartContext);', + ); + expect(header).toContain( + 'fn set_title(&mut self, title: String, cx: &mut FixtureChartContext);', + ); + }); + + test('generates the context the trait methods take', () => { + expect(header).toContain('pub struct FixtureChartContext {'); + expect(header).toContain('host: HostContext,'); + expect(header).toContain( + "pub fn events(&mut self) -> FixtureChartEvents<'_> {", + ); + expect(header).toContain( + 'pub fn set_intrinsic_size(&mut self, width: f32, height: f32) {', + ); + expect(header).toContain( + 'pub fn set_insets(&mut self, top: f32, right: f32, bottom: f32, left: f32) {', + ); + }); + + test('generates one typed event method per event', () => { + expect(header).toContain('pub struct OnPointSelected {'); + expect(header).toContain('pub index: i32,'); + expect(header).toContain('pub label: String,'); + expect(header).toContain( + 'pub fn point_selected(&mut self, value: OnPointSelected) {', + ); + expect(header).toContain('pub fn done(&mut self) {'); + expect(header).toContain('pub struct OnRichNested {'); + expect(header).toContain('pub fn rich(&mut self, value: OnRich) {'); + }); + + test('queues an event with an owned payload instead of sending it mid-call', () => { + expect(header).toContain('self.context.host.queue_event(move || {'); + expect(header).toContain('let emitter = self.context.emitter.clone();'); + expect(header).toContain('rn_gpui_fixture_chart_event_point_selected('); + expect(header).toContain( + 'pub struct FixtureChartContext {\n host: HostContext,\n emitter: EmitterHandle,\n}', + ); + }); + + test('gives a container an optional overlay that defaults to none', () => { + expect(header).toContain('fn render_overlay('); + expect(header).toContain(') -> Option {'); + expect(header).toContain('let _ = (props, window, cx);'); + }); + + test('wires the spec into core through a generated instance', () => { + expect(header).toContain( + 'struct FixtureChartInstance {', + ); + expect(header).toContain( + 'impl HostObject for FixtureChartInstance {', + ); + expect(header).toContain( + 'self.props = unsafe { FixtureChartProps::from_handle(props.as_ptr()) };', + ); + expect(header).toContain( + 'let changed = FixtureChartChanged::from_slice(changed);', + ); + expect(header).toContain( + '0 => self.component.select_point(args[0].to_i32(), &mut cx),', + ); + expect(header).toContain( + '1 => self.component.report_series(args[0].to_f64_vec(), &mut cx),', + ); + expect(header).toContain( + '2 => self.component.set_title(args[0].to_string_value(), &mut cx),', + ); + expect(header).toContain( + 'impl HostComponentSpec for FixtureChartComponent {', + ); + }); + + test('carries the declared shape on the registration value', () => { + expect(header).toContain('pub const NAME: &\'static str = "FixtureChart";'); + expect(header).toContain( + 'pub const PROPS: &\'static [&\'static str] = &[\n "lineWidth",\n "opacity",\n "ratio",\n "flag",', + ); + expect(header).toContain( + 'pub const EVENTS: &\'static [&\'static str] = &["onPointSelected", "onRich", "onDone"];', + ); + expect(header).toContain( + 'pub const COMMANDS: &\'static [&\'static str] = &["selectPoint", "reportSeries", "setTitle"];', + ); + }); +}); + +describe('the props view reads each type of the 5.2 table', () => { + test('a scalar is the value', () => { + expect(header).toContain('pub fn line_width(&self) -> i32 {'); + expect(header).toContain('pub fn opacity(&self) -> f32 {'); + expect(header).toContain('pub fn ratio(&self) -> f64 {'); + expect(header).toContain('pub fn flag(&self) -> bool {'); + expect(header).toContain( + 'fn rn_gpui_fixture_chart_props_line_width(props: *const core::ffi::c_void) -> i32;', + ); + }); + + test('a WithDefault prop is the value, with the default already applied by React Native', () => { + expect(header).toContain('pub fn tick_count(&self) -> i32 {'); + expect(header).not.toContain('pub fn tick_count(&self) -> Option'); + expect(header).toContain('pub fn mode(&self) -> FixtureChartMode {'); + }); + + test('a string union is a #[repr(i32)] enum whose discriminants match C++', () => { + expect(header).toContain('#[repr(i32)]'); + expect(header).toContain( + 'pub enum FixtureChartMode {\n /// `line`\n Line = 0,\n /// `bar`\n Bar = 1,\n}', + ); + expect(header).toContain('pub const fn as_raw(self) -> i32 {'); + expect(header).toContain('pub fn from_raw(value: i32) -> Self {'); + }); + + test('a string is borrowed as &str', () => { + expect(header).toContain('pub fn title(&self) -> &str {'); + expect(header).toContain( + 'unsafe { rn_gpui_str(rn_gpui_fixture_chart_props_title(self.handle)) }', + ); + expect(header).toContain( + 'fn rn_gpui_fixture_chart_props_title(props: *const core::ffi::c_void) -> RnGpuiStr;', + ); + expect(header).toContain('pub struct RnGpuiStr {'); + }); + + test('a ColorValue is Option', () => { + expect(header).toContain('pub fn line_color(&self) -> Option {'); + expect(header).toContain('if color.present {'); + expect(header).toContain('Some(color.value)'); + expect(header).toContain( + 'fn rn_gpui_fixture_chart_props_line_color(props: *const core::ffi::c_void) -> RnGpuiOptU32;', + ); + }); + + test('an ImageSource is a view with borrowed accessors', () => { + expect(header).toContain('pub fn icon(&self) -> RnGpuiImageSource {'); + expect(header).toContain('pub fn uri(&self) -> &str {'); + expect(header).toContain('pub fn scale(&self) -> f32 {'); + expect(header).toContain('pub fn width(&self) -> f32 {'); + expect(header).toContain('pub fn height(&self) -> f32 {'); + expect(header).toContain( + 'fn rn_gpui_image_source_uri(image: *const core::ffi::c_void) -> RnGpuiStr;', + ); + }); + + test('an array of plain structs is a borrowed &[T] of the #[repr(C)] mirror', () => { + expect(header).toContain('pub fn points(&self) -> &[FixtureChartPoints] {'); + expect(header).toContain( + 'unsafe { rn_gpui_slice(rn_gpui_fixture_chart_props_points(self.handle)) }', + ); + expect(header).toContain( + 'fn rn_gpui_fixture_chart_props_points(props: *const core::ffi::c_void) -> RnGpuiSlice;', + ); + expect(header).toContain( + 'pub struct FixtureChartPoints {\n /// `x`\n pub x: f64,\n /// `y`\n pub y: f64,\n}', + ); + }); + + test('an array of scalars is a borrowed &[T]', () => { + expect(header).toContain('pub fn values(&self) -> &[f64] {'); + expect(header).toContain( + 'fn rn_gpui_fixture_chart_props_values(props: *const core::ffi::c_void) -> RnGpuiSlice;', + ); + }); + + test('an array of strings is read one string at a time, without allocating', () => { + expect(header).toContain("pub fn labels(&self) -> RnGpuiStrArray<'_> {"); + expect(header).toContain( + 'let len = unsafe { rn_gpui_fixture_chart_props_labels_len(self.handle) };', + ); + expect(header).toContain( + 'unsafe { RnGpuiStrArray::new(self.handle, len, rn_gpui_fixture_chart_props_labels_at) }', + ); + expect(header).toContain("pub struct RnGpuiStrArray<'a> {"); + expect(header).not.toContain('RnGpuiStrSlice'); + }); + + test('an object with strings or arrays is a view with accessors', () => { + expect(header).toContain("pub struct FixtureChartCard<'a> {"); + expect(header).toContain('pub fn title(&self) -> &str {'); + expect(header).toContain( + "pub fn nested(&self) -> FixtureChartCardNested<'_> {", + ); + expect(header).not.toContain( + '#[repr(C)]\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct FixtureChartCard {', + ); + expect(header).toContain("pub struct FixtureChartCardNested<'a> {"); + expect(header).toContain('pub fn flag(&self) -> bool {'); + expect(header).toContain("pub fn names(&self) -> RnGpuiStrArray<'_> {"); + }); + + test('an array of non-plain structs is a length plus indexed views', () => { + expect(header).toContain( + "pub fn cards(&self) -> FixtureChartCardsSlice<'_> {", + ); + expect(header).toContain( + 'let len = unsafe { rn_gpui_fixture_chart_props_cards_len(self.handle) };', + ); + expect(header).toContain("pub struct FixtureChartCardsSlice<'a> {"); + expect(header).toContain( + "pub fn get(&self, index: usize) -> Option> {", + ); + expect(header).toContain( + 'let handle = unsafe { rn_gpui_fixture_chart_props_cards_at(self.handle, index) };', + ); + expect(header).toContain( + 'fn rn_gpui_fixture_chart_props_cards_len(props: *const core::ffi::c_void) -> usize;', + ); + expect(header).toContain('fn rn_gpui_fixture_chart_props_cards_at('); + expect(header).toContain('index: usize,'); + expect(header).toContain(') -> *const core::ffi::c_void;'); + }); + + test('UnsafeMixed is a Value, converted on the first read only', () => { + expect(header).toContain('pub fn extra(&self) -> &Value {'); + expect(header).toContain('mixed_extra: std::cell::OnceCell,'); + expect(header).toContain('self.mixed_extra.get_or_init(|| {'); + expect(header).toContain('rn_gpui_collect_text,'); + expect(header).toContain( + 'react_native_gpui::host_component::parse_value(&text)', + ); + expect(header).toContain('fn rn_gpui_fixture_chart_props_extra('); + expect(header).toContain('sink: RnGpuiTextSink,'); + expect(header).toContain('context: *mut core::ffi::c_void,'); + }); + + test('an array of booleans is rejected: std::vector is bit-packed', () => { + expect(() => + generateFrom(withProps(' flags?: ReadonlyArray;')), + ).toThrow( + /prop flags: an array of booleans is a bit-packed std::vector/u, + ); + }); + + test('a borrow is tied to &self, so it cannot outlive the handle', () => { + expect(header).toContain('pub fn title(&self) -> &str {'); + expect(header).toContain('pub fn points(&self) -> &[FixtureChartPoints] {'); + expect(header).toContain("pub fn card(&self) -> FixtureChartCard<'_> {"); + expect(header).not.toContain('pub fn title(&self) -> String {'); + }); +}); + +describe('the change mask', () => { + test('has one bit per prop, with a getter named after it', () => { + expect(header).toContain('words: [u64; 1],'); + expect(header).toContain('pub const fn line_width(&self) -> bool {'); + expect(header).toContain('self.words[0] & (1u64 << 0) != 0'); + expect(header).toContain('pub const fn extra(&self) -> bool {'); + expect(header).toContain('self.words[0] & (1u64 << 14) != 0'); + expect(header).not.toContain('pub const fn on_done(&self) -> bool {'); + }); + + test('reports a token with changed.report_token(), as 5.1 writes it', () => { + const reported = generateFrom(withProps(' reportToken?: Int32;')); + expect(reported).toContain('pub const fn report_token(&self) -> bool {'); + expect(reported).toContain('self.words[0] & (1u64 << 0) != 0'); + expect(header).toContain('pub const fn points(&self) -> bool {'); + }); + + test('has none/from_words/words/is_empty for core and for tests', () => { + expect(header).toContain('pub const fn none() -> Self {'); + expect(header).toContain('Self { words: [0; 1] }'); + expect(header).toContain( + 'pub const fn from_words(words: [u64; 1]) -> Self {', + ); + expect(header).toContain('pub const fn words(&self) -> [u64; 1] {'); + expect(header).toContain('pub fn is_empty(&self) -> bool {'); + expect(header).toContain('self.words.iter().all(|word| *word == 0)'); + expect(header).toContain('#[repr(transparent)]'); + }); + + test('uses one u64 up to 64 props and documents the threshold', () => { + expect(header).toContain('**Up to 64 props fit one `u64`;**'); + expect(header).toContain('pub const CHANGE_WORDS: usize = 1;'); + expect(header).not.toContain('fn rn_gpui_fixture_chart_changed('); + expect(header).toContain('pub fn from_slice(words: &[u64]) -> Self {'); + }); + + test('uses a word per 64 props above 64', () => { + const props = Array.from( + { length: 130 }, + (_, index) => ` prop${index}?: Int32;`, + ).join('\n'); + const many = generateFrom(withProps(props)); + expect(many).toContain('words: [u64; 3],'); + expect(many).toContain('pub const CHANGE_WORDS: usize = 3;'); + expect(many).toContain('pub const fn prop129(&self) -> bool {'); + expect(many).toContain('self.words[2] & (1u64 << 1) != 0'); + expect(many).not.toContain('fn rn_gpui_temp_changed('); + expect(many).toContain('pub fn from_slice(words: &[u64]) -> Self {'); + expect(many).toContain('let mut out = [0u64; 3];'); + }); + + test('is transparent over its word array, so the C++ comparison writes it in place', () => { + expect(header).toContain('#[repr(transparent)]'); + expect(header).toContain('words: [u64; 1],'); + expect(header).toMatch( + /#\[repr\(transparent\)\][\s\S]{0,200}pub struct FixtureChartChanged \{\n {4}words:/u, + ); + }); + + test('is empty for a spec with no props', () => { + const empty = generateFrom(withProps(' title?: string;')); + expect(empty).toContain('words: [u64; 1],'); + }); +}); + +describe('the generated file', () => { + test('emits the ABI types once, however many components it declares', () => { + expect(countOf(header, 'pub struct RnGpuiStr {')).toBe(1); + expect(countOf(header, 'pub struct RnGpuiSlice {')).toBe(1); + expect(countOf(header, 'pub struct RnGpuiImageSource {')).toBe(1); + const withoutImage = generateFrom(withProps(' title?: string;')); + expect(withoutImage).not.toContain('pub struct RnGpuiImageSource {'); + expect(withoutImage).toContain('pub struct RnGpuiStr {'); + }); + + test('names shims after the component, the prop and the path, for the C++ half', () => { + expect(header).toContain('fn rn_gpui_fixture_chart_props_line_width('); + expect(header).toContain('fn rn_gpui_fixture_chart_card_nested('); + expect(header).toContain('fn rn_gpui_fixture_chart_event_point_selected('); + expect(header).toContain('fn rn_gpui_fixture_chart_props_cards_len('); + expect(header).toContain('fn rn_gpui_fixture_chart_props_cards_at('); + expect(header).toContain('fn rn_gpui_fixture_chart_card_nested_names_len('); + expect(header).toContain('fn rn_gpui_fixture_chart_card_nested_names_at('); + }); + + test('converts a props string with a checked conversion, never unchecked', () => { + expect(countOf(header, 'from_utf8_unchecked(')).toBe(0); + expect(header).toContain('match core::str::from_utf8(bytes)'); + }); + + test('imports only what a component needs', () => { + expect(header).toContain('use gpui::{AnyElement, Window};'); + expect(header).toContain( + 'EmitterHandle, HostArg, HostComponentSpec, HostContext, HostObject, PropsHandle, Value,', + ); + expect(header).not.toContain('use rn_gpui_module::'); + }); + + test('still generates a module for a NativeModule spec', async () => { + const { generateRustSpec: generate } = + await import('./generate-rust-spec.js'); + const fixtureSpec = path.resolve( + path.dirname(new URL(import.meta.url).pathname), + '../../../packages/core/ReactGpui/rn-gpui-module/tests/generated/specs/NativeProbe.ts', + ); + const module = generate({ specPaths: [fixtureSpec] }); + expect(module).toContain('pub trait NativeProbeSpec:'); + expect(module).toContain('use rn_gpui_module::'); + expect(module).not.toContain('pub struct NativeProbeProps {'); + }); +}); + +describe('the generator fails, naming the prop', () => { + test('for an array of a type it cannot lay out', () => { + expect(() => + generateFrom( + withProps(' icons?: ReadonlyArray;'), + ), + ).toThrow( + /FixtureComponentSpec\.ts: prop icons: unsupported array element MixedTypeAnnotation/u, + ); + }); + + test('for a prop type it does not know', () => { + expect(() => generateFrom(withProps(' host?: HostObject;'))).toThrow( + /Unknown prop type for "host": "HostObject"/u, + ); + }); +}); diff --git a/vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.js b/vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.js new file mode 100644 index 0000000000..17fa776acc --- /dev/null +++ b/vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.js @@ -0,0 +1,2906 @@ +// Generates the Rust glue a TurboModule library implements: one trait per spec +// file, the events handle, the value structs and enums, and the descriptor that +// registers the module with `rn-gpui-module`. + +import fs from 'node:fs'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +/// `@react-native/codegen` belongs to the app, not to this package, so it is +/// resolved from the spec being generated and then from here. +export function loadTypeScriptParser(hints) { + const failures = []; + for (const hint of hints) { + try { + const require = createRequire(hint); + return require('@react-native/codegen/lib/parsers/typescript/parser.js') + .TypeScriptParser; + } catch (error) { + failures.push(`${hint}: ${error.message}`); + } + } + throw new Error( + `generate-rust-spec: cannot resolve @react-native/codegen; install it in the app.\n${failures.join('\n')}`, + ); +} + +const RUST_KEYWORDS = new Set([ + 'as', + 'break', + 'const', + 'continue', + 'crate', + 'dyn', + 'else', + 'enum', + 'extern', + 'false', + 'fn', + 'for', + 'if', + 'impl', + 'in', + 'let', + 'loop', + 'match', + 'mod', + 'move', + 'mut', + 'pub', + 'ref', + 'return', + 'self', + 'Self', + 'static', + 'struct', + 'super', + 'trait', + 'true', + 'type', + 'unsafe', + 'use', + 'where', + 'while', + 'async', + 'await', + 'dyn', + 'abstract', + 'become', + 'box', + 'do', + 'final', + 'macro', + 'override', + 'priv', + 'try', + 'typeof', + 'unsized', + 'virtual', + 'yield', +]); +const RAW_IDENTIFIER_INVALID = new Set(['crate', 'self', 'super', 'Self']); + +export function rustIdentifier(name) { + if (!RUST_KEYWORDS.has(name)) return name; + return RAW_IDENTIFIER_INVALID.has(name) ? `${name}_` : `r#${name}`; +} + +export function snakeCase(name) { + return name + .replaceAll(/([a-z0-9])([A-Z])/gu, '$1_$2') + .replaceAll(/([A-Z]+)([A-Z][a-z])/gu, '$1_$2') + .toLowerCase(); +} + +export function pascalCase(name) { + const words = name.split(/[^A-Za-z0-9]+/u).filter(Boolean); + const joined = words + .map(word => word[0].toUpperCase() + word.slice(1)) + .join(''); + if (joined.length === 0) return 'Value'; + return /^[0-9]/u.test(joined) ? `V${joined}` : joined; +} + +function variantName(name, index) { + const cleaned = name.replaceAll(/[^A-Za-z0-9]+/gu, ' ').trim(); + if (cleaned.length === 0) return `Value${index}`; + const variant = pascalCase(cleaned); + return RUST_KEYWORDS.has(variant) ? `${variant}Value` : variant; +} + +const pascal = name => pascalCase(name); + +/// A Rust string literal, so generated messages survive quotes in spec values. +const rustString = text => JSON.stringify(text); + +class SpecGenerator { + constructor({ specPath, hasteModuleName, module }) { + this.specPath = specPath; + this.hasteModuleName = hasteModuleName; + this.module = module; + this.spec = module.spec; + this.aliasMap = module.aliasMap ?? {}; + this.enumMap = module.enumMap ?? {}; + // Anonymous object types and string literal unions are named after the + // place they appear, so the same shape is generated once. + this.anonymous = new Map(); + this.types = []; + this.typeNames = new Set(); + } + + fail(where, detail) { + throw new Error(`${this.specPath}: ${where}: ${detail}`); + } + + /// The name JS asks for. The Rust names come from the spec file instead. + get moduleName() { + return this.module.moduleName ?? this.hasteModuleName; + } + + get stem() { + return path.basename(this.specPath).replace(/\.[^.]+$/u, ''); + } + + get traitName() { + return `${this.stem}Spec`; + } + + get eventsName() { + return `${this.stem}Events`; + } + + get instanceName() { + return `${this.stem}Instance`; + } + + get descriptorName() { + const stem = this.stem.replace(/^Native/u, ''); + return `native_${snakeCase(stem)}_module`; + } + + /// Maps a type annotation to a Rust type, generating a struct or enum when + /// the annotation names one. + typeOf(annotation, context) { + const { where } = context; + switch (annotation.type) { + case 'StringTypeAnnotation': + return 'String'; + case 'NumberTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'FloatTypeAnnotation': + return 'f64'; + case 'Int32TypeAnnotation': + return 'i32'; + case 'BooleanTypeAnnotation': + return 'bool'; + case 'VoidTypeAnnotation': + return '()'; + case 'NullableTypeAnnotation': + return `Option<${this.typeOf(annotation.typeAnnotation, context)}>`; + case 'ArrayTypeAnnotation': + return `Vec<${this.typeOf(annotation.elementType, context)}>`; + case 'GenericObjectTypeAnnotation': + return 'Value'; + case 'ArrayBufferTypeAnnotation': + return 'ArrayBuffer'; + case 'ObjectTypeAnnotation': + return this.objectType(annotation, context); + case 'TypeAliasTypeAnnotation': { + const alias = this.aliasMap[annotation.name]; + if (!alias) this.fail(where, `unknown type alias ${annotation.name}`); + if (alias.type === 'ObjectTypeAnnotation') { + return this.objectType(alias, { + ...context, + name: annotation.name, + aliasName: annotation.name, + }); + } + if (alias.type === 'EnumDeclaration') { + return this.enumType(annotation.name, alias); + } + this.fail(where, `unsupported type alias ${annotation.name}`); + break; + } + case 'EnumDeclaration': { + const declared = this.enumMap[annotation.name]; + if (!declared) this.fail(where, `unknown enum ${annotation.name}`); + return this.enumType(annotation.name, declared); + } + case 'UnionTypeAnnotation': + return this.unionType(annotation, context); + case 'FunctionTypeAnnotation': + if (context.callbackAllowed !== true) { + this.fail( + where, + 'a method that takes a callback must return void or a Promise', + ); + } + return this.callbackType(annotation, context); + default: + this.fail(where, `unsupported spec type ${annotation.type}`); + break; + } + return 'Value'; + } + + objectType(annotation, context) { + if (context.aliasName && this.typeNames.has(context.aliasName)) { + return context.aliasName; + } + const name = this.uniqueName( + context.aliasName ?? context.name ?? `${pascal(context.where)}Value`, + ); + // Recursion would need a boxed field, which this generator does not write. + const nested = annotation.properties ?? []; + for (const property of nested) { + if (this.referencesAlias(property.typeAnnotation, context.aliasName)) { + this.fail( + context.where, + `recursive type alias ${context.aliasName ?? name} is not supported`, + ); + } + } + this.typeNames.add(name); + const fields = nested.map(property => ({ + name: rustIdentifier(snakeCase(property.name)), + jsName: property.name, + optional: property.optional === true, + nullable: property.typeAnnotation.type === 'NullableTypeAnnotation', + annotation: property.typeAnnotation, + })); + const struct = { + kind: 'struct', + name, + fields: fields.map(field => ({ + ...field, + rustType: this.fieldType(field, { ...context, where: context.where }), + })), + }; + // Nested objects are named after their parent, so register the struct + // before generating the fields that may add more types. + this.types.push(struct); + return name; + } + + fieldType(field, context) { + const inner = this.typeOf(field.annotation, { + ...context, + name: `${context.name ?? ''}${pascal(field.jsName)}`, + aliasName: undefined, + callbackAllowed: false, + }); + if (field.optional) { + return inner.startsWith('Option<') ? inner : `Option<${inner}>`; + } + return inner; + } + + referencesAlias(annotation, aliasName) { + if (!aliasName) return false; + if (annotation.type === 'TypeAliasTypeAnnotation') { + return annotation.name === aliasName; + } + if (annotation.type === 'NullableTypeAnnotation') { + return this.referencesAlias(annotation.typeAnnotation, aliasName); + } + if (annotation.type === 'ArrayTypeAnnotation') { + return this.referencesAlias(annotation.elementType, aliasName); + } + if (annotation.type === 'ObjectTypeAnnotation') { + return (annotation.properties ?? []).some(property => + this.referencesAlias(property.typeAnnotation, aliasName), + ); + } + return false; + } + + uniqueName(base) { + let name = base; + let suffix = 2; + while (this.typeNames.has(name)) { + name = `${base}${suffix}`; + suffix += 1; + } + this.typeNames.add(name); + return name; + } + + unionType(annotation, context) { + const members = (annotation.types ?? []).map(member => { + if (member.type !== 'StringLiteralTypeAnnotation') { + this.fail(context.where, `unsupported union member ${member.type}`); + } + return { name: String(member.value), value: member.value }; + }); + if (members.length === 0) this.fail(context.where, 'empty union type'); + const name = this.uniqueName( + context.name ?? `${pascal(context.where)}Value`, + ); + this.pushEnum(name, { + memberType: 'StringTypeAnnotation', + members: members.map(member => ({ + name: member.name, + value: { type: 'StringLiteralTypeAnnotation', value: member.value }, + })), + }); + return name; + } + + enumType(name, declaration) { + if (this.typeNames.has(name)) return name; + this.typeNames.add(name); + this.pushEnum(name, declaration); + return name; + } + + pushEnum(name, declaration) { + // As React Native's own generator does, anything that is not a string enum + // is numeric. + const numeric = declaration.memberType !== 'StringTypeAnnotation'; + const members = (declaration.members ?? []).map((member, index) => { + const value = member.value?.value ?? member.name; + if (numeric && typeof value !== 'number') { + this.fail( + `enum ${name}`, + `expected a numeric member for ${member.name}`, + ); + } + if (!numeric && typeof value !== 'string') { + this.fail( + `enum ${name}`, + `expected a string member for ${member.name}`, + ); + } + return { + variant: variantName(member.name ?? String(value), index), + value, + }; + }); + this.types.push({ kind: 'enum', name, numeric, members }); + } + + callbackType(annotation, context) { + const args = (annotation.params ?? []).map((parameter, index) => { + const declared = this.typeOf(parameter.typeAnnotation, { + where: `${context.where} callback argument ${index}`, + callbackAllowed: false, + }); + return parameter.optional && !declared.startsWith('Option<') + ? `Option<${declared}>` + : declared; + }); + return `Box`; + } + + methods() { + return (this.spec.methods ?? []).map((method, index) => { + const signature = method.typeAnnotation; + const returns = signature.returnTypeAnnotation; + const where = method.name; + const kind = + returns.type === 'VoidTypeAnnotation' + ? 'void' + : returns.type === 'PromiseTypeAnnotation' + ? 'promise' + : 'value'; + if (method.name === 'getConstants' && kind === 'promise') { + this.fail(where, 'getConstants cannot return a Promise'); + } + const callbackAllowed = kind !== 'value'; + const params = (signature.params ?? []).map((parameter, position) => ({ + jsName: parameter.name, + name: rustIdentifier(snakeCase(parameter.name)), + annotation: parameter.typeAnnotation, + optional: parameter.optional === true, + nullable: parameter.typeAnnotation.type === 'NullableTypeAnnotation', + position, + isCallback: parameter.typeAnnotation.type === 'FunctionTypeAnnotation', + })); + for (const parameter of params) { + const declared = this.typeOf(parameter.annotation, { + where, + name: `${pascal(method.name)}${pascal(parameter.jsName)}`, + callbackAllowed, + }); + parameter.rustType = + parameter.optional && !declared.startsWith('Option<') + ? `Option<${declared}>` + : declared; + if (parameter.isCallback) { + parameter.callbackArgTypes = (parameter.annotation.params ?? []).map( + (callbackArg, argumentIndex) => + this.typeOf(callbackArg.typeAnnotation, { + where, + name: `${pascal(method.name)}${pascal(parameter.jsName)}Argument${argumentIndex}`, + callbackAllowed: false, + }), + ); + } + } + let rustReturn = '()'; + let returnsVoid = false; + if (kind === 'value') { + rustReturn = this.typeOf(returns, { + where, + name: `${pascal(method.name)}Result`, + callbackAllowed: false, + }); + } else if (kind === 'promise') { + const element = returns.elementType; + if (element.type === 'VoidTypeAnnotation') { + returnsVoid = true; + } else { + rustReturn = this.typeOf(element, { + where, + name: `${pascal(method.name)}Result`, + callbackAllowed: false, + }); + } + } + // A parameter the spec marks optional may be omitted, so the required + // count stops after the last one that is not. + const requiredArgs = params.reduce( + (required, parameter, position) => + parameter.optional ? required : position + 1, + 0, + ); + return { + index, + jsName: method.name, + requiredArgs, + name: rustIdentifier(snakeCase(method.name)), + kind, + params, + rustReturn, + returnsVoid, + callbackArgs: params + .filter(parameter => parameter.isCallback) + .map(parameter => parameter.position), + }; + }); + } + + events() { + return (this.spec.eventEmitters ?? []).map(eventEmitter => { + const annotation = eventEmitter.typeAnnotation.typeAnnotation; + const hasValue = annotation.type !== 'VoidTypeAnnotation'; + return { + jsName: eventEmitter.name, + name: rustIdentifier(snakeCase(eventEmitter.name)), + hasValue, + rustType: hasValue + ? this.typeOf(annotation, { + where: `event ${eventEmitter.name}`, + name: `${pascal(eventEmitter.name)}Payload`, + callbackAllowed: false, + }) + : null, + }; + }); + } +} + +/// Parses the component specs in `specPaths` into the same generators the Rust +/// half renders from, so the C++ half is derived from one model of each prop and +/// the two cannot disagree about a shape. +export function parseComponents({ specPaths }) { + const TypeScriptParser = loadTypeScriptParser([ + ...specPaths.map(specPath => path.resolve(specPath)), + path.join(process.cwd(), 'package.json'), + import.meta.url, + ]); + const parser = new TypeScriptParser(); + const found = []; + for (const specPath of specPaths) { + const schema = parser.parseFile(specPath); + for (const [hasteModuleName, module] of Object.entries( + schema.modules ?? {}, + )) { + if (module.type !== 'Component') continue; + for (const [componentName, component] of Object.entries( + module.components ?? {}, + )) { + const generator = new ComponentGenerator({ + specPath, + hasteModuleName, + componentName, + component, + }); + generator.collect(); + found.push(generator); + } + } + } + return found; +} + +export function generateRustSpec({ specPaths }) { + const TypeScriptParser = loadTypeScriptParser([ + ...specPaths.map(specPath => path.resolve(specPath)), + path.join(process.cwd(), 'package.json'), + import.meta.url, + ]); + const parser = new TypeScriptParser(); + const modules = []; + const components = []; + let componentsUseImageSource = false; + for (const specPath of specPaths) { + const schema = parser.parseFile(specPath); + const entries = Object.entries(schema.modules ?? {}); + if (entries.length !== 1) { + throw new Error( + `${specPath}: expected exactly one spec, found ${entries.length}`, + ); + } + const [hasteModuleName, module] = entries[0]; + if (module.type === 'NativeModule') { + const generator = new SpecGenerator({ + specPath, + hasteModuleName, + module, + }); + modules.push( + renderSpec(generator, generator.methods(), generator.events()), + ); + } else if (module.type === 'Component') { + for (const [componentName, component] of Object.entries( + module.components ?? {}, + )) { + const generator = new ComponentGenerator({ + specPath, + hasteModuleName, + componentName, + component, + }); + components.push(renderComponent(generator)); + componentsUseImageSource ||= generator.usesImageSource; + } + } else { + throw new Error( + `${specPath}: expected a NativeModule or a component, found ${module.type}`, + ); + } + } + const header = [ + '// Generated by @react-native-gpui/codegen from TypeScript specs; do not edit.', + '', + modules.length > 0 ? MODULE_IMPORTS : null, + components.length > 0 + ? [ + COMPONENT_IMPORTS, + componentsUseImageSource ? COMPONENT_IMAGE_SOURCE : '', + COMPONENT_ABI, + ] + .filter(part => part !== '') + .map(part => part.trimEnd()) + .join('\n') + : null, + ] + .filter(line => line !== null) + .join('\n'); + const body = [...modules, ...components] + .map(chunk => chunk.replace(/^\n+/u, '').trimEnd()) + .join('\n'); + return `${header.trimEnd()}\n\n${body}\n`; +} + +const MODULE_IMPORTS = `use std::sync::Arc; + +use rn_gpui_module::__private::{Args, FieldMap, Path}; +use rn_gpui_module::{ + ArrayBuffer, EventEmitter, EventInfo, FromValue, IntoValue, LocalBoxFuture, MethodInfo, MethodKind, + ModuleDescriptor, ModuleError, ModuleInstance, ModuleThread, Value, +}; +`; + +function renderSpec(generator, methods, events) { + const lines = []; + lines.push(`// ${generator.specPath}`); + lines.push(''); + lines.push(renderEvents(generator, events)); + lines.push(renderTrait(generator, methods)); + lines.push(renderDescriptor(generator, methods, events)); + lines.push(renderInstance(generator, methods)); + for (const type of generator.types) { + lines.push(type.kind === 'struct' ? renderStruct(type) : renderEnum(type)); + } + return lines.filter(line => line !== null).join('\n'); +} + +function renderEvents(generator, events) { + const lines = []; + lines.push('#[derive(Clone)]'); + lines.push(`pub struct ${generator.eventsName} {`); + lines.push(' emitter: EventEmitter,'); + lines.push('}'); + lines.push(''); + lines.push(`impl ${generator.eventsName} {`); + for (const event of events) { + if (event.hasValue) { + lines.push(` pub fn ${event.name}(&self, value: ${event.rustType}) {`); + lines.push( + ` self.emitter.emit(${JSON.stringify(event.jsName)}, Some(value.into_value()));`, + ); + lines.push(' }'); + } else { + lines.push(` pub fn ${event.name}(&self) {`); + lines.push( + ` self.emitter.emit(${JSON.stringify(event.jsName)}, None);`, + ); + lines.push(' }'); + } + lines.push(''); + } + lines.push(' pub fn send_event(&self, name: &str, body: Value) {'); + lines.push(' self.emitter.send_event(name, body);'); + lines.push(' }'); + lines.push('}'); + return lines.join('\n'); +} + +function methodSignature(method) { + const parameters = method.params + .map(parameter => `${parameter.name}: ${parameter.rustType}`) + .join(', '); + switch (method.kind) { + case 'value': + return ` fn ${method.name}(&self${parameters ? `, ${parameters}` : ''}) -> Result<${method.rustReturn}, ModuleError>;`; + case 'void': + return ` fn ${method.name}(&self${parameters ? `, ${parameters}` : ''});`; + default: + return ` async fn ${method.name}(&self${parameters ? `, ${parameters}` : ''}) -> Result<${method.rustReturn}, ModuleError>;`; + } +} + +function renderTrait(generator, methods) { + const lines = []; + lines.push('#[allow(async_fn_in_trait)]'); + lines.push( + `pub trait ${generator.traitName}: Send + Sync + Sized + 'static {`, + ); + lines.push(' const THREAD: ModuleThread = ModuleThread::Own;'); + lines.push(''); + lines.push(` fn new(events: ${generator.eventsName}) -> Self;`); + lines.push(''); + for (const method of methods) { + lines.push(methodSignature(method)); + } + lines.push('}'); + return lines.join('\n'); +} + +function renderDescriptor(generator, methods, events) { + const lines = []; + lines.push( + `pub fn ${generator.descriptorName}() -> ModuleDescriptor {`, + ); + lines.push(' ModuleDescriptor {'); + lines.push(` name: ${JSON.stringify(generator.moduleName)},`); + lines.push(' thread: M::THREAD,'); + lines.push(' methods: &['); + for (const method of methods) { + const kind = + method.kind === 'value' + ? 'MethodKind::Value' + : method.kind === 'void' + ? 'MethodKind::Void' + : 'MethodKind::Promise'; + const callbacks = `&[${method.callbackArgs.join(', ')}]`; + lines.push(' MethodInfo {'); + lines.push(` name: ${JSON.stringify(method.jsName)},`); + lines.push(` arg_count: ${method.params.length},`); + lines.push(` required_args: ${method.requiredArgs},`); + lines.push(` kind: ${kind},`); + lines.push(` returns_void: ${method.returnsVoid},`); + lines.push(` callback_args: ${callbacks},`); + lines.push(' },'); + } + lines.push(' ],'); + lines.push(' events: &['); + for (const event of events) { + lines.push(' EventInfo {'); + lines.push(` name: ${JSON.stringify(event.jsName)},`); + lines.push(` has_value: ${event.hasValue},`); + lines.push(' },'); + } + lines.push(' ],'); + lines.push( + ` create: |events| Arc::new(${generator.instanceName}::::new(events)) as Arc,`, + ); + lines.push(' }'); + lines.push('}'); + return lines.join('\n'); +} + +const ARGS_CURSOR = 'arguments_'; + +function argumentConversion(method, parameter) { + if (parameter.isCallback) { + const args = (parameter.annotation.params ?? []).map( + (callbackArg, index) => `a${index}: ${parameter.callbackArgTypes[index]}`, + ); + const values = (parameter.annotation.params ?? []) + .map((_, index) => `a${index}.into_value()`) + .join(', '); + return [ + ` let ${parameter.name} = ${ARGS_CURSOR}.next_callback(${JSON.stringify(parameter.jsName)})?;`, + ` let ${parameter.name}: ${parameter.rustType} =`, + ` Box::new(move |${args.join(', ')}| ${parameter.name}.invoke(vec![${values}]));`, + ]; + } + return [ + ` let ${parameter.name}: ${parameter.rustType} = ${ARGS_CURSOR}.next(${JSON.stringify(parameter.jsName)})?;`, + ]; +} + +function renderInstance(generator, methods) { + const lines = []; + lines.push(`struct ${generator.instanceName}(M);`); + lines.push(''); + lines.push(`impl ${generator.instanceName} {`); + lines.push(' fn new(events: EventEmitter) -> Self {'); + lines.push( + ` Self(M::new(${generator.eventsName} { emitter: events }))`, + ); + lines.push(' }'); + lines.push('}'); + lines.push(''); + lines.push( + `impl ModuleInstance for ${generator.instanceName} {`, + ); + for (const kind of ['value', 'void', 'promise']) { + const matching = methods.filter(method => method.kind === kind); + lines.push(renderCall(generator, kind, matching)); + lines.push(''); + } + lines.push('}'); + return lines.join('\n'); +} + +function renderCall(generator, kind, methods) { + const name = generator.moduleName; + const lines = []; + const signature = + kind === 'promise' + ? ' fn call_promise(\n self: Arc,\n method: usize,\n args: Vec,\n ) -> Result>, ModuleError> {' + : kind === 'value' + ? ' fn call_value(&self, method: usize, args: Vec) -> Result {' + : ` fn prepare_void(\n self: Arc,\n method: usize,\n args: Vec,\n ) -> Result, ModuleError> {`; + lines.push(signature); + lines.push(' match method {'); + for (const method of methods) { + lines.push(` ${method.index} => {`); + lines.push( + ` let mut ${ARGS_CURSOR} = Args::new(${JSON.stringify(name)}, ${JSON.stringify(method.jsName)}, args);`, + ); + for (const parameter of method.params) { + lines.push(...argumentConversion(method, parameter)); + } + const call = `self.0.${method.name}(${method.params.map(parameter => parameter.name).join(', ')})`; + if (kind === 'value') { + lines.push( + ` let result = rn_gpui_module::__private::catch(|| ${call})??;`, + ); + lines.push(' Ok(result.into_value())'); + } else if (kind === 'void') { + lines.push(' Ok(Box::new(move || {'); + lines.push( + ` rn_gpui_module::__private::run_void(${JSON.stringify(`${name}.${method.jsName}`)}, move || ${call});`, + ); + lines.push(' }))'); + } else { + lines.push(' Ok(Box::pin(async move {'); + lines.push( + ` let result = rn_gpui_module::__private::catch_async(${call}).await??;`, + ); + lines.push(' Ok(result.into_value())'); + lines.push(' }))'); + } + lines.push(' }'); + } + lines.push( + ` _ => Err(ModuleError::new(${JSON.stringify(`${name}: unknown method`)}))`, + ); + lines.push(' }'); + lines.push(' }'); + return lines.join('\n'); +} + +/// `Value` has no `Clone` or `PartialEq`, so a struct that holds one, directly +/// or behind `Option`/`Vec`, derives less. +function isValueType(rustType) { + const inner = rustType.endsWith('>') + ? rustType.slice(rustType.indexOf('<') + 1, -1) + : rustType; + return inner === 'Value' || (inner !== rustType && isValueType(inner)); +} + +function renderStruct(struct) { + const hasValueField = struct.fields.some(field => + isValueType(field.rustType), + ); + const derives = hasValueField + ? '#[derive(Debug)]' + : '#[derive(Debug, Clone, PartialEq)]'; + const lines = []; + lines.push(derives); + lines.push(`pub struct ${struct.name} {`); + for (const field of struct.fields) { + lines.push(` pub ${field.name}: ${field.rustType},`); + } + lines.push('}'); + lines.push(''); + lines.push(`impl FromValue for ${struct.name} {`); + lines.push( + ' fn from_value(value: Value, path: &mut Path) -> Result {', + ); + lines.push(' let fields = match value {'); + lines.push(' Value::Object(fields) => fields,'); + lines.push( + ' other => return Err(path.error(format!("expected object, got {}", other.type_name()))),', + ); + lines.push(' };'); + lines.push(' let mut fields = FieldMap::new(fields);'); + lines.push(` Ok(Self {`); + for (const field of struct.fields) { + const call = field.optional + ? `fields.take_optional::<${innerType(field.rustType)}>(path, ${JSON.stringify(field.jsName)})?` + : `fields.take::<${field.rustType}>(path, ${JSON.stringify(field.jsName)})?`; + lines.push(` ${field.name}: ${call},`); + } + lines.push(' })'); + lines.push(' }'); + lines.push('}'); + lines.push(''); + lines.push(`impl IntoValue for ${struct.name} {`); + lines.push(' fn into_value(self) -> Value {'); + lines.push(' Value::Object(vec!['); + for (const field of struct.fields) { + lines.push( + ` (${JSON.stringify(field.jsName)}.to_string(), self.${field.name}.into_value()),`, + ); + } + lines.push(' ])'); + lines.push(' }'); + lines.push('}'); + return lines.join('\n'); +} + +const innerType = rustType => + rustType.startsWith('Option<') && rustType.endsWith('>') + ? rustType.slice('Option<'.length, -1) + : rustType; + +function renderEnum(enumType) { + const lines = []; + lines.push('#[derive(Debug, Clone, Copy, PartialEq, Eq)]'); + lines.push(`pub enum ${enumType.name} {`); + for (const member of enumType.members) { + lines.push(` ${member.variant},`); + } + lines.push('}'); + lines.push(''); + lines.push(`impl FromValue for ${enumType.name} {`); + lines.push( + ' fn from_value(value: Value, path: &mut Path) -> Result {', + ); + lines.push(' match value {'); + const expected = enumType.members + .map(member => + enumType.numeric ? String(member.value) : JSON.stringify(member.value), + ) + .join(', '); + if (enumType.numeric) { + lines.push(' Value::Number(number) => match number as i64 {'); + for (const member of enumType.members) { + lines.push( + ` ${member.value} => Ok(Self::${member.variant}),`, + ); + } + lines.push( + ` other => Err(path.error(format!(${rustString(`expected one of ${expected}, got {other}`)}))),`, + ); + lines.push(' },'); + } else { + lines.push(' Value::String(text) => match text.as_str() {'); + for (const member of enumType.members) { + lines.push( + ` ${JSON.stringify(member.value)} => Ok(Self::${member.variant}),`, + ); + } + lines.push( + ` other => Err(path.error(format!(${rustString(`expected one of ${expected}, got "{other}"`)}))),`, + ); + lines.push(' },'); + } + lines.push( + ` other => Err(path.error(format!(${rustString( + enumType.numeric ? 'expected number, got {}' : 'expected string, got {}', + )}, other.type_name()))),`, + ); + lines.push(' }'); + lines.push(' }'); + lines.push('}'); + lines.push(''); + lines.push(`impl IntoValue for ${enumType.name} {`); + lines.push(' fn into_value(self) -> Value {'); + lines.push(' match self {'); + for (const member of enumType.members) { + const value = enumType.numeric + ? `Value::Number(${member.value} as f64)` + : `Value::String(${JSON.stringify(member.value)}.to_string())`; + lines.push(` Self::${member.variant} => ${value},`); + } + lines.push(' }'); + lines.push(' }'); + lines.push('}'); + return lines.join('\n'); +} + +/// The imports a component file needs. A crate that declares a host component +/// renders GPUI content, so it depends on `gpui` and on `react-native-gpui` +/// (for the host context). The module glue needs neither, so each group is +/// emitted only when it is used, and each is allowed to go unused. +const COMPONENT_IMPORTS = `#[allow(unused_imports)] +use gpui::{AnyElement, Window}; +#[allow(unused_imports)] +use react_native_gpui::host_component::{ + EmitterHandle, HostArg, HostComponentSpec, HostContext, HostObject, PropsHandle, Value, +}; +`; + +/// The C ABI between the generated Rust and the generated C++ file, emitted +/// once per generated file that declares a host component. +/// +/// Each struct is `#[repr(C)]` with pointer-sized fields in a fixed order, so +/// the C++ side can declare the same three structs and pass them by value; they +/// live once in core's `GpuiHostComponentsCpp.h`, with layout checks: +/// +/// ```c +/// struct RnGpuiStr { const uint8_t* data; size_t len; }; +/// struct RnGpuiSlice { const void* data; size_t len; }; +/// struct RnGpuiOptU32 { uint32_t value; bool present; }; +/// ``` +/// +/// Shims the C++ file implements, so both halves agree on the names: +/// +/// * props: `rn_gpui__props_`, first argument the opaque props +/// handle a `PropsHandle` owns; +/// * a generated view: `rn_gpui__`, first argument the address +/// of that C++ struct; +/// * events: `rn_gpui__event_`, first argument the opaque +/// emitter handle an `EmitterHandle` owns, then the payload's leaves as +/// arguments (a scalar by value, a string as bytes and a length, an array as a +/// pointer and a length). The C++ shim assigns them into the event struct React +/// Native generated, so the two payload layouts never have to be declared +/// twice. +/// +/// The change mask is computed in C++ against the previous shadow view and +/// arrives on the mount op, so there is no shim for it. +/// +/// What each returns: `i32` for `Int32` and for an enum's value, `f32` for +/// `Float`, `f64` for `Double`/`number`, `bool` for `boolean`, a `RnGpuiStr` +/// for a `std::string` (bytes plus length), a `RnGpuiOptU32` for a +/// `SharedColor`, the address of the field for an `ImageSource` or a struct, +/// `RnGpuiSlice` for a `std::vector` of plain data, `_len`/`_at` for a +/// `std::vector` of anything else (strings and structs), and, for a +/// `folly::dynamic`, JSON text handed to a callback. No shim allocates except +/// that last one, which serialises once per props object. +const COMPONENT_ABI = `/// A borrowed C++ \`std::string\`: bytes and an explicit length. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RnGpuiStr { + pub data: *const u8, + pub len: usize, +} + +impl RnGpuiStr { + /// A string as the event shims take one. + fn of(text: &str) -> Self { + Self { + data: text.as_ptr(), + len: text.len(), + } + } +} + +/// A borrowed C++ \`std::vector\` of plain data. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RnGpuiSlice { + pub data: *const core::ffi::c_void, + pub len: usize, +} + +/// A C++ \`SharedColor\`: \`present\` is false when React left the color unset. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct RnGpuiOptU32 { + pub value: u32, + pub present: bool, +} + +/// The bytes of a \`RnGpuiStr\`, borrowed as a \`&str\`. +/// +/// The conversion is checked: a props string came from a JS string, which an +/// engine may hand over with a lone surrogate, and an unchecked conversion of +/// invalid UTF-8 is undefined behaviour in a release build. On failure it logs +/// the offset and returns an empty string rather than reading a different +/// string than the one JS sent. +/// +/// # Safety +/// +/// \`text\` must have been read from a live C++ \`std::string\` and must outlive +/// the borrow this returns. +unsafe fn rn_gpui_str<'a>(text: RnGpuiStr) -> &'a str { + if text.data.is_null() || text.len == 0 { + return ""; + } + let bytes = core::slice::from_raw_parts(text.data, text.len); + // Checked, not \`from_utf8_unchecked\`. A release build has no \`debug_assert\`, + // and \`from_utf8\` is SIMD-fast; on the failure path the offending bytes are + // reported by offset. + match core::str::from_utf8(bytes) { + Ok(text) => text, + Err(error) => { + eprintln!( + "[host-component] a props string is not UTF-8 (valid up to byte {})", + error.valid_up_to() + ); + "" + } + } +} + +/// The elements of a \`RnGpuiSlice\`, borrowed as a \`&[T]\`. +/// +/// # Safety +/// +/// \`slice\` must point at \`slice.len\` valid, aligned \`T\` values that outlive +/// the borrow this returns. +unsafe fn rn_gpui_slice<'a, T>(slice: RnGpuiSlice) -> &'a [T] { + if slice.data.is_null() || slice.len == 0 { + return &[]; + } + core::slice::from_raw_parts(slice.data.cast::(), slice.len) +} + +/// The callback a JSON-text shim hands its bytes to: it appends them to the +/// \`String\` \`context\` points at. +unsafe extern "C" fn rn_gpui_collect_text( + context: *mut core::ffi::c_void, + data: *const u8, + len: usize, +) { + if data.is_null() || len == 0 { + return; + } + // The one copy a props read can make; counted so a test can assert no other. + react_native_gpui::host_component::note_props_bytes_copied(len); + let out = &mut *context.cast::(); + out.push_str(&String::from_utf8_lossy(core::slice::from_raw_parts(data, len))); +} + +/// The signature of that callback, as the shims take it. +type RnGpuiTextSink = unsafe extern "C" fn(*mut core::ffi::c_void, *const u8, usize); + +/// A borrowed C++ \`std::vector\`, read one string at a time. +/// +/// The strings in a props struct cannot be lent out as a \`&[&str]\` without +/// allocating, so each one is read on demand through the \`_at\` shim. +#[derive(Clone, Copy)] +pub struct RnGpuiStrArray<'a> { + owner: *const core::ffi::c_void, + len: usize, + at: unsafe extern "C" fn(*const core::ffi::c_void, usize) -> RnGpuiStr, + _borrow: core::marker::PhantomData<&'a ()>, +} + +impl<'a> RnGpuiStrArray<'a> { + /// # Safety + /// + /// \`owner\` must be a live C++ \`std::vector\`'s owner that + /// outlives \`'a\`, \`len\` its length, and \`at\` the shim that reads one + /// string. + unsafe fn new( + owner: *const core::ffi::c_void, + len: usize, + at: unsafe extern "C" fn(*const core::ffi::c_void, usize) -> RnGpuiStr, + ) -> Self { + Self { + owner, + len, + at, + _borrow: core::marker::PhantomData, + } + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + pub fn get(&self, index: usize) -> Option<&'a str> { + if index >= self.len { + return None; + } + Some(unsafe { rn_gpui_str((self.at)(self.owner, index)) }) + } + + pub fn iter(&self) -> impl Iterator + '_ { + (0..self.len).filter_map(move |index| self.get(index)) + } +} +`; + +/// React Native's C++ \`ImageSource\`, as a borrowed view. Every component uses +/// the same shape, so it is generated once per file. +const COMPONENT_IMAGE_SOURCE = `/// A borrowed view over React Native's C++ \`ImageSource\`. +#[derive(Clone, Copy)] +pub struct RnGpuiImageSource { + handle: *const core::ffi::c_void, +} + +impl RnGpuiImageSource { + /// # Safety + /// + /// \`handle\` must be the address of a live C++ \`ImageSource\` that outlives + /// the borrow the accessor returns. + unsafe fn from_handle(handle: *const core::ffi::c_void) -> Self { + Self { handle } + } + + /// The URI; empty when React did not set one. + pub fn uri(&self) -> &str { + unsafe { rn_gpui_str(rn_gpui_image_source_uri(self.handle)) } + } + + /// The bundle name, for an image bundled with the app. + pub fn bundle(&self) -> &str { + unsafe { rn_gpui_str(rn_gpui_image_source_bundle(self.handle)) } + } + + /// The image scale, 3 by default as in React Native. + pub fn scale(&self) -> f32 { + unsafe { rn_gpui_image_source_scale(self.handle) } + } + + /// The intrinsic width. + pub fn width(&self) -> f32 { + unsafe { rn_gpui_image_source_width(self.handle) } + } + + /// The intrinsic height. + pub fn height(&self) -> f32 { + unsafe { rn_gpui_image_source_height(self.handle) } + } + + /// The request body, for a remote image. + pub fn body(&self) -> &str { + unsafe { rn_gpui_str(rn_gpui_image_source_body(self.handle)) } + } + + /// The request method, for a remote image. + pub fn method(&self) -> &str { + unsafe { rn_gpui_str(rn_gpui_image_source_method(self.handle)) } + } +} + +unsafe extern "C" { + fn rn_gpui_image_source_uri(image: *const core::ffi::c_void) -> RnGpuiStr; + fn rn_gpui_image_source_bundle(image: *const core::ffi::c_void) -> RnGpuiStr; + fn rn_gpui_image_source_scale(image: *const core::ffi::c_void) -> f32; + fn rn_gpui_image_source_width(image: *const core::ffi::c_void) -> f32; + fn rn_gpui_image_source_height(image: *const core::ffi::c_void) -> f32; + fn rn_gpui_image_source_body(image: *const core::ffi::c_void) -> RnGpuiStr; + fn rn_gpui_image_source_method(image: *const core::ffi::c_void) -> RnGpuiStr; +} +`; + +/// The scalar Rust type of a codegen scalar annotation, or null when the +/// annotation is not a scalar. +function scalarType(annotation) { + switch (annotation.type) { + case 'Int32TypeAnnotation': + return 'i32'; + case 'FloatTypeAnnotation': + return 'f32'; + case 'DoubleTypeAnnotation': + case 'NumberTypeAnnotation': + return 'f64'; + case 'BooleanTypeAnnotation': + return 'bool'; + default: + return null; + } +} + +/// The name of an enum member. A string union names its own members, which is +/// also what React Native's C++ enum uses; a numeric enum has no member names in +/// the schema, so the name is built from the prop and the value, exactly as +/// React Native's own generator does (`Rank0` for `0`). +function enumMemberName(value, numeric, index, propName) { + if (!numeric) return variantName(String(value), index); + const digits = String(value).replaceAll('-', 'Minus'); + return `${pascal(propName)}${digits}`; +} + +/// One component's generated Rust: the layout mirrors, the enums, the views, +/// the props view, the change mask, the events, the context, the trait and the +/// registration value. +export class ComponentGenerator { + constructor({ specPath, hasteModuleName, componentName, component }) { + this.specPath = specPath; + this.hasteModuleName = hasteModuleName; + this.componentName = componentName; + this.component = component; + this.mirrors = []; + this.enums = []; + this.views = []; + this.slices = []; + this.payloads = []; + this.typeNames = new Set(); + this.props = []; + this.events = []; + this.commands = []; + this.shims = []; + this.usesImageSource = false; + } + + fail(where, detail) { + throw new Error(`${this.specPath}: ${where}: ${detail}`); + } + + /// The Fabric component name: what JS mounts and what core registers. + get name() { + return this.componentName; + } + + get propsName() { + return `${this.name}Props`; + } + + get changedName() { + return `${this.name}Changed`; + } + + get eventsName() { + return `${this.name}Events`; + } + + get contextName() { + return `${this.name}Context`; + } + + get specName() { + return `${this.name}Spec`; + } + + get registrationType() { + return `${this.name}Component`; + } + + get registrationFn() { + return `${snakeCase(this.name)}_component`; + } + + /// The Rust name for a nested type at `typePath`, e.g. `FixtureChartCardNested` + /// for the `nested` field of the `card` prop. + typeNameFor(typePath) { + return `${this.name}${typePath.map(pascal).join('')}`; + } + + /// React Native's own C++ name for the struct at `typePath`, so the generated + /// C++ file can `static_assert` the layouts match. + cppNameFor(typePath) { + return `${this.typeNameFor(typePath)}Struct`; + } + + get changeWords() { + return Math.max(1, Math.ceil(this.props.length / 64)); + } + + /// A type name nothing else claimed, so two props that would produce the same + /// name (`points` and `point`) get a suffix rather than colliding. + claim(base) { + let name = base; + let suffix = 2; + while (this.typeNames.has(name)) { + name = `${base}${suffix}`; + suffix += 1; + } + this.typeNames.add(name); + return name; + } + + /// The Rust enum for a codegen enum annotation, generating it once. + enumFor(propName, annotation) { + const numeric = annotation.type === 'Int32EnumTypeAnnotation'; + const options = annotation.options ?? []; + if (options.length === 0) { + this.fail(`prop ${propName}`, 'an enum needs at least one member'); + } + for (const [index, option] of options.entries()) { + if (numeric && typeof option !== 'number') { + this.fail(`prop ${propName}`, `enum member ${index} is not a number`); + } + if (!numeric && typeof option !== 'string') { + this.fail(`prop ${propName}`, `enum member ${index} is not a string`); + } + } + const name = this.claim(`${this.name}${pascal(propName)}`); + const declared = { + name, + propName, + numeric, + members: options.map((option, index) => ({ + name: enumMemberName(option, numeric, index, propName), + value: numeric ? option : index, + text: String(option), + })), + }; + this.enums.push(declared); + return declared; + } + + /// Whether every leaf of an annotation is plain data, so it can be mirrored + /// with `#[repr(C)]` and lent out as a slice of structs. + isPlain(annotation) { + switch (annotation.type) { + case 'Int32TypeAnnotation': + case 'FloatTypeAnnotation': + case 'DoubleTypeAnnotation': + case 'NumberTypeAnnotation': + case 'BooleanTypeAnnotation': + case 'StringEnumTypeAnnotation': + case 'Int32EnumTypeAnnotation': + return true; + case 'ObjectTypeAnnotation': + return (annotation.properties ?? []).every(property => + this.isPlain(property.typeAnnotation), + ); + default: + return false; + } + } + + /// A `#[repr(C)]` mirror of a plain object, named after React Native's own + /// struct for it so the C++ file can assert the two layouts match. + mirror(typePath, annotation, cppName) { + const name = this.claim(this.typeNameFor(typePath)); + const fields = (annotation.properties ?? []).map(property => ({ + jsName: property.name, + name: rustIdentifier(snakeCase(property.name)), + rustType: this.plainType(property.typeAnnotation, property.name), + })); + const mirror = { name, cppName, fields }; + this.mirrors.push(mirror); + return { kind: 'mirror', name }; + } + + /// The Rust type of a plain-data annotation, inside a mirror or a slice. + plainType(annotation, propName) { + const scalar = scalarType(annotation); + if (scalar) return scalar; + if ( + annotation.type === 'StringEnumTypeAnnotation' || + annotation.type === 'Int32EnumTypeAnnotation' + ) { + return this.enumFor(propName, annotation).name; + } + if (annotation.type === 'ObjectTypeAnnotation') { + return this.mirror([propName], annotation, null).name; + } + this.fail( + `prop ${propName}`, + `unsupported plain-data type ${annotation.type}`, + ); + return '()'; + } + + /// A view type for an object that holds strings or arrays: its fields are + /// read one at a time, so it is never mirrored. + view(typePath, annotation, cppName) { + const name = this.claim(this.typeNameFor(typePath)); + const view = { name, cppName, fields: [] }; + this.views.push(view); + view.fields = (annotation.properties ?? []).map(property => ({ + jsName: property.name, + name: rustIdentifier(snakeCase(property.name)), + shape: this.shape(property.typeAnnotation, { + typePath: [...typePath, property.name], + propName: property.name, + }), + })); + return { kind: 'view', name }; + } + + /// The shape of a props value: what its accessor returns and what the ABI + /// shim for it carries. + shape(annotation, { typePath, propName }) { + const scalar = scalarType(annotation); + if (scalar) return { kind: 'scalar', rustType: scalar }; + switch (annotation.type) { + case 'StringTypeAnnotation': + return { kind: 'string' }; + case 'StringEnumTypeAnnotation': + case 'Int32EnumTypeAnnotation': + return { + kind: 'enum', + typeName: this.enumFor(propName, annotation).name, + }; + case 'ReservedPropTypeAnnotation': + if (annotation.name === 'ColorPrimitive') return { kind: 'color' }; + if (annotation.name === 'ImageSourcePrimitive') { + this.usesImageSource = true; + return { kind: 'image' }; + } + this.fail( + `prop ${propName}`, + `unsupported reserved type ${annotation.name}`, + ); + break; + case 'MixedTypeAnnotation': + return { kind: 'mixed' }; + case 'ObjectTypeAnnotation': { + const cppName = this.cppNameFor(typePath); + if (this.isPlain(annotation)) { + return this.mirror(typePath, annotation, cppName); + } + return this.view(typePath, annotation, cppName); + } + case 'ArrayTypeAnnotation': { + const element = annotation.elementType; + if (element.type === 'StringTypeAnnotation') { + return { kind: 'array-string' }; + } + const elementScalar = scalarType(element); + if (elementScalar === 'bool') { + this.fail( + `prop ${propName}`, + 'an array of booleans is a bit-packed std::vector in C++ and cannot be borrowed; use an array of Int32', + ); + } + if (elementScalar) { + return { kind: 'array-scalar', rustType: elementScalar }; + } + if (element.type === 'ObjectTypeAnnotation') { + const cppName = this.cppNameFor(typePath); + if (this.isPlain(element)) { + return { + ...this.mirror(typePath, element, cppName), + kind: 'array-plain', + }; + } + const declared = this.view(typePath, element, cppName); + const slice = { name: `${declared.name}Slice`, view: declared.name }; + this.slices.push(slice); + return { + ...declared, + kind: 'array-view', + sliceName: slice.name, + }; + } + this.fail( + `prop ${propName}`, + `unsupported array element ${element.type}; arrays hold scalars, strings or objects`, + ); + break; + } + default: + this.fail( + `prop ${propName}`, + `unsupported component prop type ${annotation.type}`, + ); + break; + } + return { kind: 'scalar', rustType: 'f64' }; + } + + /// The shape of an event payload field. A payload travels Rust to C++ once, + /// so its fields are owned (`String`, `Vec`) rather than borrowed. + payloadShape(annotation, { payload, fieldName }) { + const scalar = scalarType(annotation); + if (scalar) return { kind: 'scalar', rustType: scalar }; + switch (annotation.type) { + case 'StringTypeAnnotation': + return { kind: 'string' }; + case 'StringEnumTypeAnnotation': + case 'Int32EnumTypeAnnotation': + return { + kind: 'enum', + typeName: this.enumFor(fieldName, annotation).name, + }; + case 'ObjectTypeAnnotation': { + const name = this.claim(`${payload}${pascal(fieldName)}`); + const declared = { + name, + label: `The \`${fieldName}\` field inside an event payload.`, + fields: [], + }; + this.payloads.push(declared); + declared.fields = (annotation.properties ?? []).map(property => ({ + jsName: property.name, + name: rustIdentifier(snakeCase(property.name)), + shape: this.payloadShape(property.typeAnnotation, { + payload: name, + fieldName: property.name, + }), + })); + return { kind: 'object', typeName: name }; + } + case 'ArrayTypeAnnotation': { + const element = annotation.elementType; + if (element.type === 'StringTypeAnnotation') { + return { kind: 'array-string' }; + } + const elementScalar = scalarType(element); + if (elementScalar) { + return { kind: 'array-scalar', rustType: elementScalar }; + } + this.fail( + `event ${payload} field ${fieldName}`, + `unsupported payload array element ${element.type}; React Native's own parser rejects those too`, + ); + break; + } + default: + this.fail( + `event ${payload} field ${fieldName}`, + `unsupported payload type ${annotation.type}`, + ); + break; + } + return { kind: 'scalar', rustType: 'f64' }; + } + + collect() { + this.props = (this.component.props ?? []).map((prop, index) => ({ + index, + jsName: prop.name, + name: rustIdentifier(snakeCase(prop.name)), + shape: this.shape(prop.typeAnnotation, { + typePath: [prop.name], + propName: prop.name, + }), + })); + + this.events = (this.component.events ?? []).map(event => { + const properties = event.typeAnnotation?.argument?.properties ?? null; + const name = rustIdentifier(snakeCase(event.name.replace(/^on/u, ''))); + if (properties === null || properties.length === 0) { + return { jsName: event.name, name, payload: null }; + } + const payload = { + name: this.claim(pascal(event.name)), + label: `The payload of the \`${event.name}\` event.`, + fields: [], + }; + this.payloads.push(payload); + this.activeLabel = payload.label; + payload.fields = properties.map(property => ({ + jsName: property.name, + name: rustIdentifier(snakeCase(property.name)), + shape: this.payloadShape(property.typeAnnotation, { + payload: payload.name, + fieldName: property.name, + }), + })); + return { jsName: event.name, name, payload }; + }); + + this.commands = (this.component.commands ?? []).map(command => ({ + jsName: command.name, + name: rustIdentifier(snakeCase(command.name)), + params: (command.typeAnnotation?.params ?? []).map(parameter => ({ + jsName: parameter.name, + name: rustIdentifier(snakeCase(parameter.name)), + rustType: this.commandType( + parameter.typeAnnotation, + command.name, + parameter.name, + ), + })), + })); + } + + /// A command argument's Rust type. The generated C++ dispatcher converts + /// `folly::dynamic` arguments to typed values once, so these + /// are owned. + commandType(annotation, commandName, parameterName) { + const scalar = scalarType(annotation); + if (scalar) return scalar; + switch (annotation.type) { + case 'StringTypeAnnotation': + return 'String'; + case 'ArrayTypeAnnotation': { + const element = this.commandType( + annotation.elementType, + commandName, + parameterName, + ); + if (!['i32', 'f32', 'f64', 'bool', 'String'].includes(element)) { + this.fail( + `command ${commandName}`, + `argument "${parameterName}" is an array of ${annotation.elementType.type}; commands take arrays of scalars or strings`, + ); + } + return `Vec<${element}>`; + } + default: + this.fail( + `command ${commandName}`, + `argument "${parameterName}" has type ${annotation.type}; commands take scalars, strings and arrays of them`, + ); + break; + } + return '()'; + } +} + +/// The indentation of an item inside a generated struct or trait. +const INDENT = ' '; +/// The indentation of a statement inside a generated method body. +const BODY = ' '; +const HANDLE = '*const core::ffi::c_void'; + +/// The width the generated Rust aims for, so a file out of this generator needs +/// no reformatting before it is read or reviewed. It matches rustfmt's default +/// `max_width`. +const WIDTH = 100; + +/// A `let Name { field: local, .. } = expression` line, wrapped when long. +function destructureLines(name, bindings, expression, indent = ' ') { + const oneLine = `let ${name} { ${bindings.join(', ')} } = ${expression};`; + const pattern = bindings.join(', '); + if (pattern.length <= 18 && indent.length + oneLine.length <= WIDTH) { + return [`${indent}${oneLine}`]; + } + return [ + `${indent}let ${name} {`, + ...bindings.map(binding => `${indent} ${binding},`), + `${indent}} = ${expression};`, + ]; +} + +/// A `&[&str]` constant, one entry per line when it would run past [`WIDTH`]. +function stringSliceLines(declaration, items, indent = INDENT) { + const oneLine = `${declaration} &[${items.map(rustString).join(', ')}];`; + if (indent.length + oneLine.length <= WIDTH) return [`${indent}${oneLine}`]; + return [ + `${indent}${declaration} &[`, + ...items.map(item => `${indent} ${rustString(item)},`), + `${indent}];`, + ]; +} + +/// One expression statement at method-body indentation, wrapped inside a block +/// when it would run past [`WIDTH`]. +function expressionLines(expression, indent = BODY) { + if (indent.length + expression.length <= WIDTH) { + return [`${indent}${expression}`]; + } + const unsafe = /^unsafe \{ (.*) \}$/u.exec(expression); + if (unsafe !== null) { + return [`${indent}unsafe {`, `${indent} ${unsafe[1]}`, `${indent}}`]; + } + return [`${indent}{`, `${indent} ${expression}`, `${indent}}`]; +} + +/// An `unsafe { shim(..) }` statement, one argument per line when it would run +/// past [`WIDTH`]. +function unsafeCallLines(callee, args, indent = BODY) { + const oneLine = `${indent}unsafe { ${callee}(${args.join(', ')}) }`; + if (oneLine.length <= WIDTH) return [oneLine]; + return [ + `${indent}unsafe {`, + ...(args.map(argument => `${indent} ${argument},`).length > 0 + ? [ + `${indent} ${callee}(`, + ...args.map(argument => `${indent} ${argument},`), + `${indent} )`, + ] + : [`${indent} ${callee}()`]), + `${indent}}`, + ]; +} + +/// A declaration, wrapped one parameter per line when it would run long. +function wrapDeclaration(declaration, indent = ' ') { + if (declaration.length <= 100) return [declaration]; + const open = declaration.indexOf('('); + const close = declaration.lastIndexOf(')'); + if (open === -1 || close === -1) return [declaration]; + const head = declaration.slice(0, open + 1); + const body = declaration.slice(open + 1, close); + const tail = declaration.slice(close); + const params = []; + let depth = 0; + let start = 0; + for (let index = 0; index < body.length; index += 1) { + const character = body[index]; + if ('<(['.includes(character)) depth += 1; + else if ('>)]'.includes(character)) depth -= 1; + else if (character === ',' && depth === 0) { + params.push(body.slice(start, index).trim()); + start = index + 1; + } + } + const last = body.slice(start).trim(); + if (last.length > 0) params.push(last); + return [ + head, + ...params.map(param => `${indent} ${param},`), + `${indent}${tail}`, + ]; +} + +/// A Rust doc comment block from lines of text. +function docComment(lines, indent = '') { + return lines.map(line => `${indent}/// ${line}`.trimEnd()).join('\n'); +} + +/// A doc comment saying what an accessor returns. +function accessorDoc(shape) { + switch (shape.kind) { + case 'scalar': + return `The value, as \`${shape.rustType}\`.`; + case 'string': + return 'The string, borrowed from the C++ field.'; + case 'enum': + return `The enum value, as \`${shape.typeName}\`.`; + case 'color': + return 'The ARGB color, or `None` when React left it unset.'; + case 'image': + return 'A borrowed view over the C++ `ImageSource`.'; + case 'mixed': + return 'The `UnsafeMixed` value, parsed once on the first read: the one accessor that copies.'; + case 'array-scalar': + return `The elements, borrowed, as \`&[${shape.rustType}]\`.`; + case 'array-string': + return 'The strings, read one at a time.'; + case 'mirror': + return `The value, as the \`#[repr(C)]\` mirror \`${shape.name}\`.`; + case 'array-plain': + return `The elements, borrowed, as \`&[${shape.name}]\`.`; + case 'view': + return 'A borrowed view over the C++ struct.'; + case 'array-view': + return 'The length, then one borrowed view per index.'; + default: + return ''; + } +} + +/// One accessor on a props view or a view type: its body, and the `extern "C"` +/// declaration of the shim it calls. +function accessor({ shimPrefix, shape, name, jsName, owner }) { + const shim = `${shimPrefix}_${snakeCase(jsName)}`; + const lines = []; + const externs = []; + const call = args => `unsafe { ${shim}(${args}) }`; + switch (shape.kind) { + case 'scalar': + lines.push(` pub fn ${name}(&self) -> ${shape.rustType} {`); + lines.push(` ${call('self.handle')}`); + lines.push(' }'); + externs.push(` fn ${shim}(${owner}: ${HANDLE}) -> ${shape.rustType};`); + break; + case 'string': + lines.push(` pub fn ${name}(&self) -> &str {`); + lines.push( + ...expressionLines(`unsafe { rn_gpui_str(${shim}(self.handle)) }`), + ); + lines.push(' }'); + externs.push(` fn ${shim}(${owner}: ${HANDLE}) -> RnGpuiStr;`); + break; + case 'enum': + lines.push(` pub fn ${name}(&self) -> ${shape.typeName} {`); + lines.push(` ${shape.typeName}::from_raw(${call('self.handle')})`); + lines.push(' }'); + externs.push(` fn ${shim}(${owner}: ${HANDLE}) -> i32;`); + break; + case 'color': + lines.push(` pub fn ${name}(&self) -> Option {`); + lines.push(` let color = ${call('self.handle')};`); + lines.push(' if color.present {'); + lines.push(' Some(color.value)'); + lines.push(' } else {'); + lines.push(' None'); + lines.push(' }'); + lines.push(' }'); + externs.push(` fn ${shim}(${owner}: ${HANDLE}) -> RnGpuiOptU32;`); + break; + case 'image': + lines.push(` pub fn ${name}(&self) -> RnGpuiImageSource {`); + lines.push( + ...expressionLines( + `unsafe { RnGpuiImageSource::from_handle(${shim}(self.handle)) }`, + ), + ); + lines.push(' }'); + externs.push(` fn ${shim}(${owner}: ${HANDLE}) -> ${HANDLE};`); + break; + case 'mixed': + lines.push(` pub fn ${name}(&self) -> &Value {`); + lines.push(` self.mixed_${name}.get_or_init(|| {`); + lines.push(' let mut text = String::new();'); + lines.push(' unsafe {'); + lines.push(` ${shim}(`); + lines.push(' self.handle,'); + lines.push(' rn_gpui_collect_text,'); + lines.push(' (&mut text as *mut String).cast(),'); + lines.push(' );'); + lines.push(' }'); + lines.push( + ' react_native_gpui::host_component::parse_value(&text)', + ); + lines.push(' })'); + lines.push(' }'); + externs.push( + ` fn ${shim}(${owner}: ${HANDLE}, sink: RnGpuiTextSink, context: *mut core::ffi::c_void);`, + ); + break; + case 'array-scalar': + lines.push(` pub fn ${name}(&self) -> &[${shape.rustType}] {`); + lines.push(` unsafe { rn_gpui_slice(${shim}(self.handle)) }`); + lines.push(' }'); + externs.push(` fn ${shim}(${owner}: ${HANDLE}) -> RnGpuiSlice;`); + break; + case 'array-string': + lines.push(` pub fn ${name}(&self) -> RnGpuiStrArray<'_> {`); + lines.push(` let len = unsafe { ${shim}_len(self.handle) };`); + lines.push( + ` unsafe { RnGpuiStrArray::new(self.handle, len, ${shim}_at) }`, + ); + lines.push(' }'); + externs.push(` fn ${shim}_len(${owner}: ${HANDLE}) -> usize;`); + externs.push( + ` fn ${shim}_at(${owner}: ${HANDLE}, index: usize) -> RnGpuiStr;`, + ); + break; + case 'mirror': + lines.push(` pub fn ${name}(&self) -> ${shape.name} {`); + lines.push(` ${call('self.handle')}`); + lines.push(' }'); + externs.push(` fn ${shim}(${owner}: ${HANDLE}) -> ${shape.name};`); + break; + case 'array-plain': + lines.push(` pub fn ${name}(&self) -> &[${shape.name}] {`); + lines.push(` unsafe { rn_gpui_slice(${shim}(self.handle)) }`); + lines.push(' }'); + externs.push(` fn ${shim}(${owner}: ${HANDLE}) -> RnGpuiSlice;`); + break; + case 'view': + lines.push(` pub fn ${name}(&self) -> ${shape.name}<'_> {`); + lines.push( + ...expressionLines( + `unsafe { ${shape.name}::from_handle(${shim}(self.handle)) }`, + ), + ); + lines.push(' }'); + externs.push(` fn ${shim}(${owner}: ${HANDLE}) -> ${HANDLE};`); + break; + case 'array-view': + lines.push(` pub fn ${name}(&self) -> ${shape.sliceName}<'_> {`); + lines.push(` let len = unsafe { ${shim}_len(self.handle) };`); + lines.push(` ${shape.sliceName}::new(self.handle, len)`); + lines.push(' }'); + externs.push(` fn ${shim}_len(${owner}: ${HANDLE}) -> usize;`); + externs.push( + ` fn ${shim}_at(${owner}: ${HANDLE}, index: usize) -> ${HANDLE};`, + ); + shape.atShim = `${shim}_at`; + break; + default: + throw new Error( + `generate-rust-spec: unsupported accessor shape ${shape.kind}`, + ); + } + return { lines, externs }; +} + +/// The trait signature of one command. +function commandSignature(command, contextName) { + const params = command.params + .map(parameter => `${parameter.name}: ${parameter.rustType}, `) + .join(''); + return ` fn ${command.name}(&mut self, ${params}cx: &mut ${contextName});`; +} + +function renderMirror(mirror) { + const lines = []; + lines.push( + docComment([ + `Layout mirror of the C++ \`${mirror.cppName}\`.`, + '', + 'Plain data only, so the generated C++ file asserts its size and every', + 'field offset against this struct, and a `std::vector` of them is lent out', + 'as a slice of these.', + ]), + ); + lines.push('#[repr(C)]'); + lines.push('#[derive(Clone, Copy, Debug, PartialEq)]'); + lines.push(`pub struct ${mirror.name} {`); + for (const field of mirror.fields) { + lines.push(` /// \`${field.jsName}\``); + lines.push(` pub ${field.name}: ${field.rustType},`); + } + lines.push('}'); + return lines; +} + +function renderComponentEnum(declared) { + const lines = []; + lines.push( + docComment([ + 'Mirror of the `enum class` React Native generates for this spec enum.', + '', + 'The discriminants match the C++ enum, so the value crosses the ABI as an', + '`i32` and the generated C++ file can assert the two agree.', + ]), + ); + lines.push('#[repr(i32)]'); + lines.push('#[derive(Clone, Copy, Debug, PartialEq, Eq)]'); + lines.push(`pub enum ${declared.name} {`); + for (const member of declared.members) { + lines.push(` /// \`${member.text}\``); + lines.push(` ${member.name} = ${member.value},`); + } + lines.push('}'); + lines.push(''); + lines.push(`impl ${declared.name} {`); + lines.push(' /// The value the C++ enum holds.'); + lines.push(' pub const fn as_raw(self) -> i32 {'); + lines.push(' self as i32'); + lines.push(' }'); + lines.push(''); + lines.push( + ' /// The member for a C++ enum value. The C++ side only ever sends a', + ); + lines.push( + ' /// declared member, so a mismatch means the two generated halves', + ); + lines.push( + ' /// disagree: a debug build trips here, a release build keeps the', + ); + lines.push( + ' /// first member rather than reading an invalid discriminant.', + ); + lines.push(' pub fn from_raw(value: i32) -> Self {'); + lines.push(' match value {'); + for (const member of declared.members) { + lines.push(` ${member.value} => Self::${member.name},`); + } + lines.push(' other => {'); + lines.push( + ` debug_assert!(false, "invalid ${declared.name} value {other}");`, + ); + lines.push(` Self::${declared.members[0].name}`); + lines.push(' }'); + lines.push(' }'); + lines.push(' }'); + lines.push('}'); + return lines; +} + +/// The Rust type of an event payload field. +function payloadRustType(shape) { + switch (shape.kind) { + case 'scalar': + return shape.rustType; + case 'string': + return 'String'; + case 'enum': + return shape.typeName; + case 'object': + return shape.typeName; + case 'array-scalar': + return `Vec<${shape.rustType}>`; + case 'array-string': + return 'Vec'; + default: + return '()'; + } +} + +function renderPayload(payload) { + const lines = []; + lines.push( + docComment([ + payload.label, + '', + 'Owned, because an event travels Rust to C++ once. The shim fills the C++', + 'event struct React Native generated from it, one leaf at a time, rather', + 'than passing this struct across the ABI:', + '', + '- a scalar by value, an enum as its `i32`;', + '- a `String` as a pointer and a length;', + '- a `Vec` as a pointer and a length, and a `Vec` through a', + ' temporary `RnGpuiStr` slice.', + ]), + ); + lines.push('#[derive(Clone, Debug, PartialEq)]'); + lines.push(`pub struct ${payload.name} {`); + for (const field of payload.fields) { + lines.push(` /// \`${field.jsName}\``); + lines.push(` pub ${field.name}: ${payloadRustType(field.shape)},`); + } + lines.push('}'); + return lines; +} + +/// The lines that flatten an owned payload into shim arguments, the shim's +/// parameter list, and the argument expressions. +function flattenPayload(generator, payload, indent = BODY) { + const params = []; + const lines = []; + const args = []; + + const walkValue = (field, local, argument) => { + const shape = field.shape; + switch (shape.kind) { + case 'scalar': + params.push(`${argument}: ${shape.rustType}`); + args.push(local); + return; + case 'enum': + params.push(`${argument}: i32`); + args.push(`${local}.as_raw()`); + return; + case 'string': + params.push(`${argument}_data: *const u8`); + params.push(`${argument}_len: usize`); + args.push(`${local}.as_ptr()`); + args.push(`${local}.len()`); + return; + case 'array-scalar': + params.push(`${argument}_data: *const ${shape.rustType}`); + params.push(`${argument}_len: usize`); + args.push(`${local}.as_ptr()`); + args.push(`${local}.len()`); + return; + case 'array-string': { + const strings = `__${argument}_strings`; + const head = `${indent}let ${strings}: Vec = ${local}`; + if ( + `${head}.iter().map(|text| RnGpuiStr::of(text)).collect();`.length <= + WIDTH + ) { + lines.push( + `${head}.iter().map(|text| RnGpuiStr::of(text)).collect();`, + ); + } else { + lines.push(head); + lines.push(`${indent} .iter()`); + lines.push(`${indent} .map(|text| RnGpuiStr::of(text))`); + lines.push(`${indent} .collect();`); + } + params.push(`${argument}_data: *const RnGpuiStr`); + params.push(`${argument}_len: usize`); + args.push(`${strings}.as_ptr()`); + args.push(`${strings}.len()`); + return; + } + case 'object': { + const declared = generator.payloads.find( + candidate => candidate.name === shape.typeName, + ); + walkObject(declared, local, argument); + return; + } + default: + throw new Error( + `generate-rust-spec: unsupported event payload shape ${shape.kind}`, + ); + } + }; + + const walkObject = (declared, expression, prefix) => { + const locals = declared.fields.map( + field => `__${prefix === '' ? '' : `${prefix}_`}${field.name}`, + ); + lines.push( + ...destructureLines( + declared.name, + declared.fields.map( + (field, index) => `${field.name}: ${locals[index]}`, + ), + expression, + indent, + ), + ); + declared.fields.forEach((field, index) => { + const argument = prefix === '' ? field.name : `${prefix}_${field.name}`; + walkValue(field, locals[index], argument); + }); + }; + + walkObject(payload, 'value', ''); + return { params, lines, args }; +} + +function renderComponentEvents(generator) { + const lines = []; + lines.push( + docComment([ + `The typed events of a \`${generator.name}\` node.`, + '', + 'An event is queued, not sent: the payload is owned by a closure that', + "joins the store's one ordered report queue and runs when the store", + 'delivers it. The closure calls the generated `extern "C"` shim for the', + 'event, which fills the C++ event struct React Native generated and hands it', + 'to the generated emitter. The payload crosses as its leaves — a scalar by', + 'value, a string as bytes and a length, an array as a pointer and a length —', + 'so no JSON is built on the way.', + ]), + ); + lines.push(`pub struct ${generator.eventsName}<'a> {`); + lines.push(` context: &'a mut ${generator.contextName},`); + lines.push('}'); + lines.push(''); + lines.push(`impl ${generator.eventsName}<'_> {`); + let first = true; + for (const event of generator.events) { + const shim = `rn_gpui_${snakeCase(generator.name)}_event_${snakeCase(event.name)}`; + if (!first) lines.push(''); + first = false; + lines.push(` /// Queues \`${event.jsName}\`.`); + if (event.payload === null) { + lines.push(` pub fn ${event.name}(&mut self) {`); + lines.push(' let emitter = self.context.emitter.clone();'); + lines.push(' self.context.host.queue_event(move || {'); + lines.push(` unsafe { ${shim}(emitter.as_ptr()) }`); + lines.push(' });'); + lines.push(' }'); + generator.shims.push(` fn ${shim}(emitter: ${HANDLE});`); + continue; + } + const { + params, + lines: flatten, + args, + } = flattenPayload(generator, event.payload, ' '); + lines.push( + ` pub fn ${event.name}(&mut self, value: ${event.payload.name}) {`, + ); + lines.push(' let emitter = self.context.emitter.clone();'); + lines.push(' self.context.host.queue_event(move || {'); + lines.push(...flatten); + lines.push( + ...unsafeCallLines(shim, ['emitter.as_ptr()', ...args], ' '), + ); + lines.push(' });'); + lines.push(' }'); + generator.shims.push( + ` fn ${shim}(${['emitter: ' + HANDLE, ...params].join(', ')});`, + ); + } + lines.push('}'); + return lines; +} + +function renderContext(generator) { + const lines = []; + lines.push( + docComment([ + `What a [\`${generator.specName}\`] method is given: core's host context plus`, + "the component's typed events.", + '', + "Core builds one per call from the node's [`HostContext`] and the event", + 'emitter handle it took at create. What the call reports goes straight', + "into the store's queue, which delivers it after the batch, the command or", + 'the frame that produced it.', + ]), + ); + lines.push(`pub struct ${generator.contextName} {`); + lines.push(' host: HostContext,'); + lines.push(' emitter: EmitterHandle,'); + lines.push('}'); + lines.push(''); + lines.push(`impl ${generator.contextName} {`); + lines.push( + " /// Core builds one for a call: the node's host context and emitter.", + ); + lines.push( + ' pub fn new(host: HostContext, emitter: EmitterHandle) -> Self {', + ); + lines.push(' Self { host, emitter }'); + lines.push(' }'); + lines.push(''); + lines.push(" /// Core's context: the node's tag and what it reported."); + lines.push(' pub fn host(&self) -> &HostContext {'); + lines.push(' &self.host'); + lines.push(' }'); + lines.push(''); + lines.push( + " /// Core's context, mutably, for a component that reads back what it", + ); + lines.push(' /// reported in this call.'); + lines.push(' pub fn host_mut(&mut self) -> &mut HostContext {'); + lines.push(' &mut self.host'); + lines.push(' }'); + lines.push(''); + lines.push(' /// The Fabric tag of the node.'); + lines.push(' pub fn tag(&self) -> i32 {'); + lines.push(' self.host.tag'); + lines.push(' }'); + lines.push(''); + lines.push(' /// Reports the size this component wants, in DIPs.'); + lines.push( + ' pub fn set_intrinsic_size(&mut self, width: f32, height: f32) {', + ); + lines.push(' self.host.set_intrinsic_size(width, height);'); + lines.push(' }'); + lines.push(''); + lines.push( + " /// Reports the padding this container's chrome needs around React's", + ); + lines.push(' /// children, in DIPs.'); + lines.push( + ' pub fn set_insets(&mut self, top: f32, right: f32, bottom: f32, left: f32) {', + ); + lines.push(' self.host.set_insets(top, right, bottom, left);'); + lines.push(' }'); + lines.push(''); + lines.push(" /// The component's typed events."); + lines.push(` pub fn events(&mut self) -> ${generator.eventsName}<'_> {`); + lines.push(` ${generator.eventsName} { context: self }`); + lines.push(' }'); + lines.push('}'); + return lines; +} + +function renderView(generator, view) { + const lines = []; + lines.push( + docComment([ + `A borrowing view over the C++ \`${view.cppName}\`.`, + '', + 'Its fields are read one at a time through generated accessors, because a', + 'struct holding a `std::string` or a `std::vector` has no plain layout to', + 'mirror.', + ]), + ); + const mixedFields = view.fields.filter(field => field.shape.kind === 'mixed'); + lines.push(`pub struct ${view.name}<'a> {`); + lines.push(` handle: ${HANDLE},`); + lines.push(" _borrow: core::marker::PhantomData<&'a ()>,"); + for (const field of mixedFields) { + lines.push(` mixed_${field.name}: std::cell::OnceCell,`); + } + lines.push('}'); + lines.push(''); + lines.push(`impl<'a> ${view.name}<'a> {`); + lines.push(' /// # Safety'); + lines.push(' ///'); + lines.push( + ` /// \`handle\` must be the address of a live C++ \`${view.cppName}\` that`, + ); + lines.push( + ' /// outlives the borrow the accessor that returned this view gave.', + ); + lines.push(` unsafe fn from_handle(handle: ${HANDLE}) -> Self {`); + lines.push(' Self {'); + lines.push(' handle,'); + lines.push(' _borrow: core::marker::PhantomData,'); + for (const field of mixedFields) { + lines.push(` mixed_${field.name}: std::cell::OnceCell::new(),`); + } + lines.push(' }'); + lines.push(' }'); + for (const field of view.fields) { + const rendered = accessor({ + shimPrefix: `rn_gpui_${snakeCase(view.name)}`, + shape: field.shape, + name: field.name, + jsName: field.jsName, + owner: 'value', + }); + lines.push(''); + lines.push(` /// ${accessorDoc(field.shape)}`); + lines.push(...rendered.lines); + generator.shims.push(...rendered.externs); + } + lines.push('}'); + return lines; +} + +function renderSlice(slice, view, atShim) { + const lines = []; + lines.push( + docComment([ + `A borrowed C++ \`std::vector<${view.cppName}>\`:`, + 'the length, then one view per index.', + ]), + ); + lines.push(`pub struct ${slice.name}<'a> {`); + lines.push(` handle: ${HANDLE},`); + lines.push(' len: usize,'); + lines.push(" _borrow: core::marker::PhantomData<&'a ()>,"); + lines.push('}'); + lines.push(''); + lines.push(`impl<'a> ${slice.name}<'a> {`); + lines.push(` fn new(handle: ${HANDLE}, len: usize) -> Self {`); + lines.push(' Self {'); + lines.push(' handle,'); + lines.push(' len,'); + lines.push(' _borrow: core::marker::PhantomData,'); + lines.push(' }'); + lines.push(' }'); + lines.push(''); + lines.push(' pub fn len(&self) -> usize {'); + lines.push(' self.len'); + lines.push(' }'); + lines.push(''); + lines.push(' pub fn is_empty(&self) -> bool {'); + lines.push(' self.len == 0'); + lines.push(' }'); + lines.push(''); + lines.push( + ` pub fn get(&self, index: usize) -> Option<${view.name}<'a>> {`, + ); + lines.push(' if index >= self.len {'); + lines.push(' return None;'); + lines.push(' }'); + lines.push(` let handle = unsafe { ${atShim}(self.handle, index) };`); + lines.push(' if handle.is_null() {'); + lines.push(' return None;'); + lines.push(' }'); + lines.push(` Some(unsafe { ${view.name}::from_handle(handle) })`); + lines.push(' }'); + lines.push(''); + lines.push( + ` pub fn iter(&self) -> impl Iterator> + '_ {`, + ); + lines.push(' (0..self.len).filter_map(move |index| self.get(index))'); + lines.push(' }'); + lines.push('}'); + return lines; +} + +function renderPropsView(generator) { + const mixed = generator.props.filter(prop => prop.shape.kind === 'mixed'); + const lines = []; + lines.push( + docComment([ + `A typed, borrowing view over the C++ \`${generator.name}Props\`.`, + '', + 'It wraps the `shared_ptr` the mount op carries, as an', + 'opaque handle. Every accessor reads the C++ field in place, so no prop is', + 'serialised, copied or parsed on the commit path. A borrow lives as long as', + 'the `&self` it came from, so a read cannot outlive the handle.', + '', + 'A component that keeps data past the call copies what it needs in', + '`create`/`update` instead of holding the view.', + ]), + ); + lines.push(`pub struct ${generator.propsName} {`); + lines.push(` handle: ${HANDLE},`); + for (const prop of mixed) { + lines.push(` mixed_${prop.name}: std::cell::OnceCell,`); + } + lines.push('}'); + lines.push(''); + lines.push(`impl ${generator.propsName} {`); + lines.push(' /// # Safety'); + lines.push(' ///'); + lines.push( + ` /// \`handle\` must be the node's \`shared_ptr\``, + ); + lines.push( + ' /// and must stay alive for as long as the view is used: core holds it', + ); + lines.push(' /// until the next update replaces it.'); + lines.push(` pub unsafe fn from_handle(handle: ${HANDLE}) -> Self {`); + if (mixed.length === 0) { + lines.push(' Self { handle }'); + } else { + lines.push(' Self {'); + lines.push(' handle,'); + for (const prop of mixed) { + lines.push(` mixed_${prop.name}: std::cell::OnceCell::new(),`); + } + lines.push(' }'); + } + lines.push(' }'); + for (const prop of generator.props) { + const rendered = accessor({ + shimPrefix: `rn_gpui_${snakeCase(generator.name)}_props`, + shape: prop.shape, + name: prop.name, + jsName: prop.jsName, + owner: 'props', + }); + lines.push(''); + lines.push(` /// ${accessorDoc(prop.shape)}`); + lines.push(...rendered.lines); + generator.shims.push(...rendered.externs); + } + lines.push('}'); + return lines; +} + +function renderChanged(generator) { + const words = generator.changeWords; + const lines = []; + lines.push( + docComment([ + 'Which props changed in an update.', + '', + `One bit per prop, in \`${generator.registrationType}::PROPS\` order: bit \`i\` is`, + 'the `i`-th prop, bit 0 of word 0 first. **Up to 64 props fit one `u64`;**', + 'more use one word per 64 (`CHANGE_WORDS` words in all), so 65 props take', + 'two words, 128 take two, 129 take three.', + '', + 'C++ computes it against the previous shadow view and the mount op carries', + 'the words, so it is always relative to the props this component last saw', + 'and needs no translation.', + ]), + ); + lines.push('#[repr(transparent)]'); + lines.push('#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]'); + lines.push(`pub struct ${generator.changedName} {`); + lines.push(` words: [u64; ${words}],`); + lines.push('}'); + lines.push(''); + lines.push(`impl ${generator.changedName} {`); + lines.push( + ' /// No prop changed. Core never calls `update` with an empty mask.', + ); + lines.push(' pub const fn none() -> Self {'); + lines.push(` Self { words: [0; ${words}] }`); + lines.push(' }'); + lines.push(''); + lines.push(' /// A mask from the words the C++ comparison wrote.'); + lines.push(` pub const fn from_words(words: [u64; ${words}]) -> Self {`); + lines.push(' Self { words }'); + lines.push(' }'); + lines.push(''); + lines.push( + ' /// A mask from the words the mount op carried. Missing words are zero.', + ); + lines.push(' pub fn from_slice(words: &[u64]) -> Self {'); + lines.push(` let mut out = [0u64; ${words}];`); + lines.push(' for (slot, word) in out.iter_mut().zip(words) {'); + lines.push(' *slot = *word;'); + lines.push(' }'); + lines.push(' Self { words: out }'); + lines.push(' }'); + lines.push(''); + lines.push(' /// The words.'); + lines.push(` pub const fn words(&self) -> [u64; ${words}] {`); + lines.push(' self.words'); + lines.push(' }'); + lines.push(''); + lines.push(' /// Whether no prop changed at all.'); + lines.push(' pub fn is_empty(&self) -> bool {'); + lines.push(' self.words.iter().all(|word| *word == 0)'); + lines.push(' }'); + for (const prop of generator.props) { + const word = Math.floor(prop.index / 64); + const bit = prop.index % 64; + lines.push(''); + lines.push(` /// Whether the \`${prop.jsName}\` prop changed.`); + lines.push(` pub const fn ${prop.name}(&self) -> bool {`); + lines.push(` self.words[${word}] & (1u64 << ${bit}) != 0`); + lines.push(' }'); + } + lines.push('}'); + return lines; +} + +function renderSpecTrait(generator) { + const lines = []; + lines.push( + docComment([ + `What a package implements to render a \`${generator.name}\` node.`, + '', + 'Everything takes `&mut self` and the context rather than an `App`, because', + 'the store is leased while a batch is applied: a component that dispatched', + 'during a call would re-enter it, so the context records what to do and core', + 'applies it when the call returns.', + ]), + ); + lines.push(`pub trait ${generator.specName}: Sized + 'static {`); + lines.push( + ' /// Created once, when the node mounts, on the main thread, with the', + ); + lines.push(" /// node's full props."); + lines.push( + ` fn create(props: &${generator.propsName}, cx: &mut ${generator.contextName}) -> Self;`, + ); + lines.push(''); + lines.push(" /// The node's props changed."); + lines.push(' ///'); + lines.push( + ' /// Core calls this only when at least one prop changed — never for a', + ); + lines.push( + ' /// layout-only, state-only or reparenting update — and `changed` is', + ); + lines.push(' /// never empty.'); + lines.push(' fn update('); + lines.push(' &mut self,'); + lines.push(` props: &${generator.propsName},`); + lines.push(` changed: ${generator.changedName},`); + lines.push(` cx: &mut ${generator.contextName},`); + lines.push(' );'); + lines.push(''); + lines.push( + " /// The GPUI content, laid out inside the node's Yoga frame.", + ); + lines.push(' ///'); + lines.push( + ' /// Must be cheap: read `props` by reference and keep derived data in', + ); + lines.push( + " /// `create`/`update`. Core paints a container's React children itself", + ); + lines.push(' ///, so they are not passed here.'); + lines.push(' fn render('); + lines.push(' &mut self,'); + lines.push(` props: &${generator.propsName},`); + lines.push(' window: &mut Window,'); + lines.push(` cx: &mut ${generator.contextName},`); + lines.push(' ) -> AnyElement;'); + lines.push(''); + lines.push( + " /// Content painted above a container's React children (a focus ring, a", + ); + lines.push(' /// badge), if the component has any.'); + lines.push(' ///'); + lines.push( + ' /// Core calls this only for a component declared `container: true`.', + ); + lines.push(' fn render_overlay('); + lines.push(' &mut self,'); + lines.push(` props: &${generator.propsName},`); + lines.push(' window: &mut Window,'); + lines.push(` cx: &mut ${generator.contextName},`); + lines.push(' ) -> Option {'); + lines.push(' let _ = (props, window, cx);'); + lines.push(' None'); + lines.push(' }'); + for (const command of generator.commands) { + lines.push(''); + lines.push(` /// JS dispatched the \`${command.jsName}\` command.`); + lines.push(' ///'); + lines.push( + ' /// The arguments are already typed: the generated C++ dispatcher', + ); + lines.push( + ' /// converted them once, and an unknown command or a wrong argument', + ); + lines.push(' /// type never reaches here.'); + lines.push(commandSignature(command, generator.contextName)); + } + lines.push('}'); + return lines; +} + +function renderRegistration(generator) { + const lines = []; + lines.push( + docComment([ + `The registration value for [\`${generator.specName}\`].`, + '', + `\`react_native_gpui::host_component::register_component(${generator.registrationFn}::())\``, + 'registers `T` for the Fabric component [`NAME`](Self::NAME) before the', + 'first surface starts. The component must be declared under', + '`platforms.desktop.hostComponents` in `react-native.config.js`: autolink', + 'gives it its registry index and its layout flags from there.', + ]), + ); + lines.push( + `pub struct ${generator.registrationType} {`, + ); + lines.push(' _spec: core::marker::PhantomData T>,'); + lines.push('}'); + lines.push(''); + lines.push( + `/// The value \`register_component\` takes for the \`${generator.name}\` component.`, + ); + lines.push( + `pub const fn ${generator.registrationFn}() -> ${generator.registrationType} {`, + ); + lines.push(` ${generator.registrationType} {`); + lines.push(' _spec: core::marker::PhantomData,'); + lines.push(' }'); + lines.push('}'); + lines.push(''); + lines.push( + `impl ${generator.registrationType} {`, + ); + lines.push(' /// The Fabric component name the spec declares.'); + lines.push( + ` pub const NAME: &'static str = ${rustString(generator.name)};`, + ); + lines.push(''); + lines.push( + " /// The spec's own props, in change-mask bit order. `ViewProps` are", + ); + lines.push(" /// core's, not these."); + lines.push( + ...stringSliceLines( + "pub const PROPS: &'static [&'static str] =", + generator.props.map(prop => prop.jsName), + ), + ); + lines.push(''); + lines.push(' /// How many `u64` words the change mask has.'); + lines.push(` pub const CHANGE_WORDS: usize = ${generator.changeWords};`); + lines.push(''); + lines.push(' /// The events the spec declares, in schema order.'); + lines.push( + ...stringSliceLines( + "pub const EVENTS: &'static [&'static str] =", + generator.events.map(event => event.jsName), + ), + ); + lines.push(''); + lines.push(' /// The commands the spec declares, in schema order.'); + lines.push( + ...stringSliceLines( + "pub const COMMANDS: &'static [&'static str] =", + generator.commands.map(command => command.jsName), + ), + ); + lines.push('}'); + return lines; +} + +function renderComponentInstance(generator) { + const lines = []; + const instance = `${generator.name}Instance`; + const spec = generator.specName; + lines.push( + docComment([ + `One mounted \`${generator.name}\` node: the user's component, the props`, + 'view built once for the props version it was last given, and the handles', + 'that keep the C++ props and emitter alive.', + '', + 'Core stores it as a `Box` on the node, so after the node is', + 'created every call is a vtable call and nothing looks the component up.', + ]), + ); + lines.push(`struct ${instance} {`); + lines.push(' component: T,'); + lines.push(` props: ${generator.propsName},`); + lines.push( + ' /// Owns the C++ props `props` reads. Declared after it, so it drops last.', + ); + lines.push(' #[allow(dead_code)]'); + lines.push(' handle: PropsHandle,'); + lines.push(' emitter: EmitterHandle,'); + lines.push('}'); + lines.push(''); + lines.push(`impl HostObject for ${instance} {`); + lines.push( + ' fn update(&mut self, props: PropsHandle, changed: &[u64], cx: HostContext) {', + ); + lines.push( + ' // The new view is built once and kept: the `UnsafeMixed` caches in it', + ); + lines.push(' // then serialise at most once per props version.'); + lines.push( + ` self.props = unsafe { ${generator.propsName}::from_handle(props.as_ptr()) };`, + ); + lines.push( + ' // The previous props are released only now, after the view stopped', + ); + lines.push(' // pointing at them.'); + lines.push(' self.handle = props;'); + lines.push( + ` let mut cx = ${generator.contextName}::new(cx, self.emitter.clone());`, + ); + lines.push( + ` let changed = ${generator.changedName}::from_slice(changed);`, + ); + lines.push(' self.component.update(&self.props, changed, &mut cx);'); + lines.push(' }'); + lines.push(''); + lines.push( + ' fn render(&mut self, window: &mut Window, cx: HostContext) -> AnyElement {', + ); + lines.push( + ` let mut cx = ${generator.contextName}::new(cx, self.emitter.clone());`, + ); + lines.push(' self.component.render(&self.props, window, &mut cx)'); + lines.push(' }'); + lines.push(''); + lines.push( + ' fn render_overlay(&mut self, window: &mut Window, cx: HostContext) -> Option {', + ); + lines.push( + ` let mut cx = ${generator.contextName}::new(cx, self.emitter.clone());`, + ); + lines.push( + ' self.component.render_overlay(&self.props, window, &mut cx)', + ); + lines.push(' }'); + lines.push(''); + lines.push( + ' fn command(&mut self, index: u32, args: &[HostArg], cx: HostContext) {', + ); + if (generator.commands.length === 0) { + lines.push(' let _ = (index, args, cx);'); + } else { + lines.push( + ` let mut cx = ${generator.contextName}::new(cx, self.emitter.clone());`, + ); + lines.push(' match index {'); + generator.commands.forEach((command, commandIndex) => { + const values = command.params.map((parameter, position) => { + const argument = `args[${position}]`; + return commandArgumentExpression(parameter.rustType, argument); + }); + const call = `self.component.${command.name}(${[...values, '&mut cx'].join(', ')})`; + lines.push(` ${commandIndex} => ${call},`); + }); + lines.push(' _ => {}'); + lines.push(' }'); + } + lines.push(' }'); + lines.push('}'); + lines.push(''); + lines.push( + `impl HostComponentSpec for ${generator.registrationType} {`, + ); + lines.push(` const NAME: &'static str = ${rustString(generator.name)};`); + lines.push(''); + lines.push( + ' fn create(props: PropsHandle, emitter: EmitterHandle, cx: HostContext) -> Box {', + ); + lines.push( + ` let view = unsafe { ${generator.propsName}::from_handle(props.as_ptr()) };`, + ); + lines.push( + ` let mut cx = ${generator.contextName}::new(cx, emitter.clone());`, + ); + lines.push(' let component = T::create(&view, &mut cx);'); + lines.push(` Box::new(${instance} {`); + lines.push(' component,'); + lines.push(' props: view,'); + lines.push(' handle: props,'); + lines.push(' emitter,'); + lines.push(' })'); + lines.push(' }'); + lines.push('}'); + return lines; +} + +/// How a command argument is read from the `HostArg` core hands the instance. +/// The C++ dispatcher validated the type, so the read is infallible. +function commandArgumentExpression(rustType, argument) { + switch (rustType) { + case 'i32': + return `${argument}.to_i32()`; + case 'f32': + return `${argument}.to_f32()`; + case 'f64': + return `${argument}.to_f64()`; + case 'bool': + return `${argument}.to_bool()`; + case 'String': + return `${argument}.to_string_value()`; + case 'Vec': + return `${argument}.to_i32_vec()`; + case 'Vec': + return `${argument}.to_f32_vec()`; + case 'Vec': + return `${argument}.to_f64_vec()`; + case 'Vec': + return `${argument}.to_bool_vec()`; + case 'Vec': + return `${argument}.to_string_vec()`; + default: + throw new Error( + `generate-rust-spec: unsupported command argument ${rustType}`, + ); + } +} + +/// Renders one component: its types first, then the API over them. +function renderComponent(generator) { + generator.collect(); + const lines = []; + lines.push(''); + lines.push(`// ${generator.specPath} — ${generator.name}`); + + for (const mirror of generator.mirrors) { + lines.push(''); + lines.push(...renderMirror(mirror)); + } + for (const declared of generator.enums) { + lines.push(''); + lines.push(...renderComponentEnum(declared)); + } + for (const payload of generator.payloads) { + lines.push(''); + lines.push(...renderPayload(payload)); + } + for (const view of generator.views) { + lines.push(''); + lines.push(...renderView(generator, view)); + } + lines.push(''); + lines.push(...renderPropsView(generator)); + const atShims = new Map(); + const recordShim = shape => { + if (shape.sliceName !== undefined && shape.atShim !== undefined) { + atShims.set(shape.sliceName, shape.atShim); + } + }; + for (const prop of generator.props) recordShim(prop.shape); + for (const view of generator.views) { + for (const field of view.fields) recordShim(field.shape); + } + for (const slice of generator.slices) { + const view = generator.views.find( + candidate => candidate.name === slice.view, + ); + lines.push(''); + lines.push(...renderSlice(slice, view, atShims.get(slice.name))); + } + lines.push(''); + lines.push(...renderChanged(generator)); + lines.push(''); + lines.push(...renderComponentEvents(generator)); + lines.push(''); + lines.push(...renderContext(generator)); + lines.push(''); + lines.push(...renderSpecTrait(generator)); + lines.push(''); + lines.push(...renderRegistration(generator)); + lines.push(''); + lines.push(...renderComponentInstance(generator)); + lines.push(''); + lines.push('// The shims the generated C++ file implements.'); + lines.push( + '// The ABI comment at the top of this file gives the naming and the shapes.', + ); + lines.push('#[allow(dead_code, clippy::all)]'); + lines.push('unsafe extern "C" {'); + for (const shim of generator.shims) { + const declaration = shim.startsWith(' fn ') + ? shim + : ` ${shim.trim()}`; + lines.push(...wrapDeclaration(declaration)); + } + lines.push('}'); + return lines.join('\n'); +} + +/// Writes the generated file only when it changed. +export function writeRustSpec({ specPaths, outputPath }) { + const contents = generateRustSpec({ specPaths }); + const existing = fs.existsSync(outputPath) + ? fs.readFileSync(outputPath, 'utf8') + : null; + if (existing !== contents) { + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, contents); + } + return contents; +} + +const invokedDirectly = + process.argv[1] !== undefined && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); + +if (invokedDirectly) { + const args = process.argv.slice(2); + if (args.length < 2) { + console.error('usage: generate-rust-spec ... '); + process.exit(2); + } + const outputPath = args.at(-1); + writeRustSpec({ specPaths: args.slice(0, -1), outputPath }); +} diff --git a/vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.test.js b/vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.test.js new file mode 100644 index 0000000000..e586a6b250 --- /dev/null +++ b/vendor/react-native-gpui/packages/codegen/src/generate-rust-spec.test.js @@ -0,0 +1,273 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, test } from 'vitest'; +import { + generateRustSpec, + snakeCase, + pascalCase, +} from './generate-rust-spec.js'; + +// The fixture spec is also what the integration crate compiles, so the two +// stay in step. +const fixtureSpec = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../packages/core/ReactGpui/rn-gpui-module/tests/generated/specs/NativeProbe.ts', +); + +const generated = () => generateRustSpec({ specPaths: [fixtureSpec] }); + +const generateFor = spec => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'rust-spec-')); + const file = path.join(directory, 'NativeTemp.ts'); + fs.writeFileSync(file, spec); + return generateRustSpec({ specPaths: [file] }); +}; + +const fixture = fs.readFileSync(fixtureSpec, 'utf8'); + +test('names are converted to Rust', () => { + expect(snakeCase('greetLater')).toBe('greet_later'); + expect(snakeCase('addListener')).toBe('add_listener'); + expect(snakeCase('URLForID')).toBe('url_for_id'); + expect(pascalCase('onProgress')).toBe('OnProgress'); + expect(pascalCase('no-op')).toBe('NoOp'); +}); + +describe('the generated glue', () => { + const header = generated(); + + test('describes the spec with a trait and a descriptor', () => { + expect(header).toContain('pub trait NativeProbeSpec:'); + expect(header).toContain('fn new(events: NativeProbeEvents) -> Self;'); + expect(header).toContain( + 'pub fn native_probe_module() -> ModuleDescriptor {', + ); + expect(header).toContain('name: "NativeProbe"'); + expect(header).toContain('thread: M::THREAD'); + expect(header).toContain( + 'create: |events| Arc::new(NativeProbeInstance::::new(events))', + ); + }); + + test('maps every type of the table', () => { + expect(header).toContain( + 'fn scalar(&self, text: String, count: f64, whole: i32, flag: bool) -> Result;', + ); + // An optional parameter and a nullable one. + expect(header).toContain( + 'fn optional_value(&self, maybe: Option, nullable: Option) -> Result, ModuleError>;', + ); + expect(header).toContain( + 'fn lists(&self, items: Vec, readonly_items: Vec) -> Result, ModuleError>;', + ); + // A named alias, used twice. + expect(header).toContain( + 'fn options(&self, value: Options, again: Options) -> Result;', + ); + // A TypeScript enum and a literal union. + expect(header).toContain( + 'fn level(&self, level: Level) -> Result;', + ); + // A numeric enum, and names that are Rust keywords. + expect(header).toContain( + 'fn rank(&self, value: Rank) -> Result;', + ); + expect(header).toContain( + 'fn r#match(&self, text: String) -> Result;', + ); + expect(header).toContain('pub r#type: String,'); + // `Object` and `UnsafeObject` stay untyped. + expect(header).toContain( + 'fn anything(&self, value: Value) -> Result;', + ); + // A void method, a Promise, and Promise. + expect(header).toContain('fn nothing(&self);'); + expect(header).toContain( + 'async fn later(&self, text: String) -> Result;', + ); + expect(header).toContain( + 'async fn done(&self) -> Result<(), ModuleError>;', + ); + // A callback parameter. + expect(header).toContain( + 'fn lookup(&self, key: String, callback: Box);', + ); + }); + + test('generates the events handle and the descriptor events', () => { + expect(header).toContain('pub struct NativeProbeEvents {'); + expect(header).toContain('pub fn on_progress(&self, value: f64) {'); + expect(header).toContain( + 'self.emitter.emit("onProgress", Some(value.into_value()));', + ); + expect(header).toContain('pub fn on_done(&self) {'); + expect(header).toContain('self.emitter.emit("onDone", None);'); + expect(header).toContain( + 'pub fn send_event(&self, name: &str, body: Value) {', + ); + expect(header).toContain('EventInfo {'); + expect(header).toContain('name: "onDone"'); + expect(header).toContain('has_value: false'); + }); + + test('generates a struct for a named alias with both conversions', () => { + expect(header).toContain('pub struct Options {'); + expect(header).toContain('pub label: Option,'); + expect(header).toContain('pub count: i32,'); + expect(header).toContain('pub mode: OptionsMode,'); + expect(header).toContain('impl FromValue for Options {'); + expect(header).toContain('fields.take_optional::(path, "label")?'); + expect(header).toContain('impl IntoValue for Options {'); + // The inline object of `inline(value: { x: number; y?: string })`. + expect(header).toContain('pub struct InlineValue {'); + expect(header).toContain('pub x: f64,'); + expect(header).toContain('pub y: Option,'); + // The nested object inside the alias. + expect(header).toContain('pub struct OptionsNested {'); + }); + + test('maps an ArrayBuffer to the crate type, in arguments and results', () => { + const mapped = generateFor( + fixture + .replace( + 'nothing(): void;', + 'nothing(buffer: ArrayBuffer): void;\n bytes(): ArrayBuffer;', + ) + .replace( + 'readonly onProgress: CodegenTypes.EventEmitter;', + 'readonly onProgress: CodegenTypes.EventEmitter;', + ), + ); + expect(mapped).toContain('fn nothing(&self, buffer: ArrayBuffer);'); + expect(mapped).toContain( + 'fn bytes(&self) -> Result;', + ); + expect(mapped).toContain('pub fn on_progress(&self, value: ArrayBuffer)'); + expect(mapped).toContain('ArrayBuffer, EventEmitter, EventInfo'); + }); + + test('drops the unusable derives when a struct holds an untyped field', () => { + expect(header).toContain('pub struct Meta {'); + expect(header).toContain('pub extra: Option,'); + expect(header).toContain('pub list: Option>,'); + expect(header).not.toContain('Clone, PartialEq)]\npub struct Meta'); + }); + + test('generates a numeric enum from a TypeScript enum', () => { + expect(header).toContain('pub enum Rank {'); + expect(header).toContain('Value::Number(number) => match number as i64 {'); + expect(header).toContain('0 => Ok(Self::Low),'); + expect(header).toContain('Self::High => Value::Number(1 as f64),'); + expect(header).toContain('expected one of 0, 1, got {other}'); + }); + + test('generates enums for a TypeScript enum and a literal union', () => { + expect(header).toContain('pub enum Level {'); + expect(header).toContain('Low,'); + expect(header).toContain('Self::Low => Value::String("low".to_string()),'); + expect(header).toContain('pub enum OptionsMode {'); + expect(header).toContain('"left" => Ok(Self::Left),'); + }); + + test('routes each method through the entry point of its kind', () => { + expect(header).toContain( + 'let result = rn_gpui_module::__private::catch(|| self.0.scalar(text, count, whole, flag))??;', + ); + // A void method is prepared on the calling thread and run on the module's. + expect(header).toContain( + 'rn_gpui_module::__private::run_void("NativeProbe.nothing", move || self.0.nothing());', + ); + expect(header).toContain( + " fn prepare_void(\n self: Arc,\n method: usize,\n args: Vec,\n ) -> Result, ModuleError> {", + ); + expect(header).toContain( + 'let result = rn_gpui_module::__private::catch_async(self.0.later(text)).await??;', + ); + expect(header).toContain('kind: MethodKind::Promise,'); + // `optionalValue(maybe?, nullable)` needs both arguments, `nothing()` none. + expect(header).toContain( + 'arg_count: 2,\n required_args: 2,', + ); + expect(header).toContain( + 'arg_count: 0,\n required_args: 0,', + ); + expect(header).toContain('returns_void: true,'); + expect(header).toContain('callback_args: &[1],'); + // The callback is built from a RawCallback and consumes it. The cursor has + // a name of its own, so an object parameter cannot shadow it. + expect(header).toContain( + 'let callback = arguments_.next_callback("callback")?;', + ); + expect(header).toContain( + 'callback.invoke(vec![a0.into_value(), a1.into_value()])', + ); + }); +}); + +describe('the generator fails', () => { + test('for a callback parameter on a value-returning method', () => { + expect(() => + generateFor( + fixture.replace( + 'lookup(key: string, callback: (value: string, found: boolean) => void): void;', + 'lookup(key: string, callback: (value: string, found: boolean) => void): string;', + ), + ), + ).toThrow( + /NativeTemp\.ts: lookup: a method that takes a callback must return void or a Promise/u, + ); + }); + + test('for a type it cannot map, naming the method', () => { + expect(() => + generateFor( + fixture.replace('nothing(): void;', 'nothing(rank: 1 | 2): void;'), + ), + ).toThrow( + /NativeTemp\.ts: nothing: unsupported union member NumberLiteralTypeAnnotation/u, + ); + }); + + test('for a host object, naming the spec', () => { + // React Native's parser rejects host types before the generator sees them. + expect(() => + generateFor( + fixture.replace('nothing(): void;', 'nothing(host: HostObject): void;'), + ), + ).toThrow(/NativeTemp: Unrecognized generic type 'HostObject'/u); + }); + + test('for a recursive alias', () => { + expect(() => + generateFor( + fixture.replace( + 'export type Options = {', + 'export type Options = {\n child?: Options;', + ), + ), + ).toThrow(/recursive type alias Options is not supported/u); + }); +}); + +test('reads a callback after an object parameter', () => { + const generated = + generateFor(`import {TurboModuleRegistry, type TurboModule} from 'react-native'; + +export type Args = { + title?: string; + buttons?: Array; +}; + +export interface Spec extends TurboModule { + readonly show: (args: Args, callback: (id: number, value: string) => void) => void; +} + +export default TurboModuleRegistry.getEnforcing('Temp'); +`); + expect(generated).toContain('let args: Args = arguments_.next("args")?;'); + expect(generated).toContain( + 'let callback = arguments_.next_callback("callback")?;', + ); +}); diff --git a/vendor/react-native-gpui/packages/codegen/src/kitchen-sink-spec.js b/vendor/react-native-gpui/packages/codegen/src/kitchen-sink-spec.js new file mode 100644 index 0000000000..a8f9a50fae --- /dev/null +++ b/vendor/react-native-gpui/packages/codegen/src/kitchen-sink-spec.js @@ -0,0 +1,56 @@ +export const KITCHEN_SINK_SPEC = `import { + codegenNativeCommands, + codegenNativeComponent, + type CodegenTypes, + type ColorValue, + type ImageSource, + type ViewProps, +} from 'react-native'; + +export interface NativeProps extends ViewProps { + lineWidth?: Int32; + opacity?: Float; + ratio?: Double; + flag?: boolean; + tickCount?: CodegenTypes.WithDefault; + mode?: CodegenTypes.WithDefault<'line' | 'bar', 'line'>; + title?: string; + lineColor?: ColorValue; + icon?: ImageSource; + points?: ReadonlyArray>; + values?: ReadonlyArray; + labels?: ReadonlyArray; + card?: Readonly<{ + title: string; + nested: Readonly<{ flag: boolean; names: ReadonlyArray }>; + }>; + cards?: ReadonlyArray>; + extra?: CodegenTypes.UnsafeMixed; + onPointSelected?: CodegenTypes.DirectEventHandler< + Readonly<{ index: Int32; x: Double; label: string }> + >; + onRich?: CodegenTypes.DirectEventHandler< + Readonly<{ nested: Readonly<{ flag: boolean; names: string[] }> }> + >; + onDone?: CodegenTypes.DirectEventHandler; +} + +export interface FixtureChartCommands { + selectPoint: (ref: React.ComponentRef, index: Int32) => void; + reportSeries: ( + ref: React.ComponentRef, + values: Array, + ) => void; + setTitle: (ref: React.ComponentRef, title: string) => void; +} + +export const Commands = codegenNativeCommands({ + supportedCommands: ['selectPoint', 'reportSeries', 'setTitle'], +}); + +const FixtureChart: any = null; + +export default codegenNativeComponent('FixtureChart', { + interfaceOnly: true, +}); +`; diff --git a/vendor/react-native-gpui/packages/core/NativeViewHost.tsx b/vendor/react-native-gpui/packages/core/NativeViewHost.tsx new file mode 100644 index 0000000000..66d2d9e14f --- /dev/null +++ b/vendor/react-native-gpui/packages/core/NativeViewHost.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import type { ViewProps } from 'react-native'; + +import NativeViewHostNativeComponent from './NativeViewHostNativeComponent'; + +export type NativeViewHostProps = ViewProps & { + viewName: string; + nativeProps?: Record; +}; + +export default function NativeViewHost({ + viewName, + nativeProps, + ...props +}: NativeViewHostProps): React.JSX.Element { + return ( + + ); +} diff --git a/vendor/react-native-gpui/packages/core/NativeViewHostNativeComponent.ts b/vendor/react-native-gpui/packages/core/NativeViewHostNativeComponent.ts new file mode 100644 index 0000000000..f9927940fc --- /dev/null +++ b/vendor/react-native-gpui/packages/core/NativeViewHostNativeComponent.ts @@ -0,0 +1,10 @@ +import { codegenNativeComponent, type ViewProps } from 'react-native'; + +export interface NativeViewHostNativeProps extends ViewProps { + viewName: string; + nativePropsJson?: string; +} + +export default codegenNativeComponent( + 'NativeViewHost', +); diff --git a/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/devsupport/DevServerHelper.cpp b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/devsupport/DevServerHelper.cpp new file mode 100644 index 0000000000..b234dae95e --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/devsupport/DevServerHelper.cpp @@ -0,0 +1,235 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Override: resolved from the include root, because this copy is +// compiled from ReactCommonOverrides/ rather than next to the header. +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// Override: the cxx platform hardcodes the +// Android bundle identifier, which makes Metro serve the Android JS surface. +constexpr std::string_view DEFAULT_PLATFORM = "desktop"; + +std::string SHA256(const std::string& input) { + std::array hash{}; + SHA256_CTX sha256; + SHA256_Init(&sha256); + SHA256_Update(&sha256, input.c_str(), input.size()); + SHA256_Final(hash.data(), &sha256); + std::stringstream ss; + for (unsigned char i : hash) { + ss << std::hex << std::setw(2) << std::setfill('0') << (int)i; + } + return ss.str(); +} + +std::string urlEscape(const std::string& str) { + std::regex pattern("[^a-zA-Z0-9._~-]"); + return std::regex_replace(str, pattern, ""); +} + +} // namespace + +namespace facebook::react { + +DevServerHelper::DevServerHelper( + std::string appId, + std::string deviceName, + std::string devServerHost, + uint32_t devServerPort, + const HttpClientFactory& httpClientFactory, + JavaScriptModuleCallback javaScriptModuleCallback) noexcept + : appId_(std::move(appId)), + deviceName_(std::move(deviceName)), + devServerHost_(std::move(devServerHost)), + devServerPort_(devServerPort), + httpClient_(httpClientFactory()), + javaScriptModuleCallback_(std::move(javaScriptModuleCallback)) { + deviceId_ = SHA256(fmt::format("{}-{}", deviceName_, appId_)); +} + +std::future DevServerHelper::downloadBundleResourceSync( + const std::string& jsBundleUrl, + DownloadProgressCallback&& downloadProgressCallback) { + auto promise = std::make_shared>(); + // What the server said about the response, and whether the promise has been + // settled: a body that is not a bundle must reject it, and a response that + // completes without a body must not leave the caller waiting forever. + struct ResponseState { + uint16_t status{0}; + std::string contentType; + std::atomic settled{false}; + }; + auto state = std::make_shared(); + auto reject = [promise, state](const std::string& message) { + if (state->settled.exchange(true)) { + return; + } + LOG(WARNING) << message; + try { + throw std::runtime_error(message); + } catch (...) { + try { + promise->set_exception(std::current_exception()); + } catch (...) { + } + } + }; + http::NetworkCallbacks callbacks{ + .onResponse = + [state]( + uint16_t responseCode, + http::Headers headers, + const std::string& /*responseUrl*/) { + state->status = responseCode; + for (auto& [name, value] : headers) { + std::string lower = name; + std::transform( + lower.begin(), lower.end(), lower.begin(), [](char c) { + return static_cast(std::tolower(c)); + }); + if (lower == "content-type") { + state->contentType = value; + } + } + }, + .onBody = + [jsBundleUrl, promise, state, reject]( + std::unique_ptr ioBuf) { + std::string responseStr = ioBuf->moveToFbString().toStdString(); + // Something other than Metro may be listening on this port (another + // project's Metro, for example), and Metro itself answers a failed + // build with a JSON error. Either way the body is not a bundle, and + // handing it to the engine fails with a syntax error and a blank + // window. Reject it, so ReactHost falls back to the built-in bundle. + const bool okStatus = state->status == 0 || + (state->status >= 200 && state->status < 300); + const bool javascriptType = state->contentType.empty() || + state->contentType.find("javascript") != std::string::npos; + const auto first = responseStr.find_first_not_of(" \t\r\n"); + const bool looksLikeData = first != std::string::npos && + (responseStr[first] == '{' || responseStr[first] == '<'); + if (!okStatus || !javascriptType || looksLikeData) { + reject(fmt::format( + "The dev server at {} did not return a JS bundle (status {}, " + "content type '{}'): {}", + jsBundleUrl, + state->status, + state->contentType, + responseStr.substr(0, 300))); + return; + } + if (!state->settled.exchange(true)) { + promise->set_value(std::move(responseStr)); + } + }, + .onResponseComplete = + [jsBundleUrl, promise, state, reject, downloadProgressCallback]( + const std::string& error, bool timeoutError) { + if (error.empty() && !timeoutError && !state->settled.load()) { + if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::FAILED); + } + reject( + "The dev server at " + jsBundleUrl + + " returned an empty response"); + return; + } + if (!error.empty() || timeoutError) { + if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::FAILED); + } + reject( + "Failed to download JS bundle from Url: " + jsBundleUrl + + ". Error: " + (error.empty() ? "Timeout" : error)); + } else if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::FINISHED); + } + }}; + httpClient_->sendRequest(std::move(callbacks), "GET", jsBundleUrl); + if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::STARTED); + } + return promise->get_future(); +} + +std::string DevServerHelper::getInspectorUrl() const { + bool isProfilingBuild = + jsinspector_modern::InspectorFlags::getInstance().getIsProfilingBuild(); + + return fmt::format( + "ws://{}:{}/inspector/device?name={}&app={}&device={}&profiling={}", + devServerHost_, + devServerPort_, + urlEscape(deviceName_), + appId_, + deviceId_, + isProfilingBuild); +} + +std::string DevServerHelper::getBundleUrl() const { + if (sourcePath_.empty()) { + return ""; + } + + bool dev = true; + bool lazy = dev; + bool minify = false; + bool splitBundle = false; + bool modulesOnly = splitBundle; + bool runModule = !splitBundle; + return fmt::format( + "http://{}:{}/{}.bundle?platform={}&dev={}&lazy={}&minify={}&app={}&modulesOnly={}&runModule={}&inlineSourceMap=false&excludeSource=true&sourcePaths=url-server", + devServerHost_, + devServerPort_, + sourcePath_, + DEFAULT_PLATFORM, + dev, + lazy, + minify, + appId_, + modulesOnly, + runModule); +}; + +std::string DevServerHelper::getPackagerConnectionUrl() const { + return fmt::format("ws://{}:{}/message", devServerHost_, devServerPort_); +} + +void DevServerHelper::openDebugger() const { + auto requestUrl = fmt::format( + "http://{}:{}/open-debugger?device={}", + devServerHost_, + devServerPort_, + deviceId_); + httpClient_->sendRequest({}, "POST", requestUrl); +} + +void DevServerHelper::setupHMRClient() const { + folly::dynamic params = folly::dynamic::array( + DEFAULT_PLATFORM, + sourcePath_, + devServerHost_, + devServerPort_, + true /*enable*/); + javaScriptModuleCallback_("HMRClient", "setup", std::move(params)); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/nativemodule/dom/NativeDOM.cpp b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/nativemodule/dom/NativeDOM.cpp new file mode 100644 index 0000000000..2d30287655 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/nativemodule/dom/NativeDOM.cpp @@ -0,0 +1,485 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include +#include + +#ifdef RN_DISABLE_OSS_PLUGIN_HEADER +#include "Plugins.h" +#endif + +std::shared_ptr NativeDOMModuleProvider( + std::shared_ptr jsInvoker) { + return std::make_shared(std::move(jsInvoker)); +} + +namespace facebook::react { + +namespace { +inline std::shared_ptr getShadowNode( + facebook::jsi::Runtime& runtime, + jsi::Value& shadowNodeValue) { + return Bridging>::fromJs( + runtime, shadowNodeValue); +} + +#pragma mark - Private helpers + +UIManager& getUIManagerFromRuntime(facebook::jsi::Runtime& runtime) { + return UIManagerBinding::getBinding(runtime)->getUIManager(); +} + +RootShadowNode::Shared getCurrentShadowTreeRevision( + facebook::jsi::Runtime& runtime, + SurfaceId surfaceId) { + auto shadowTreeRevisionProvider = + getUIManagerFromRuntime(runtime).getShadowTreeRevisionProvider(); + return shadowTreeRevisionProvider->getCurrentRevision(surfaceId); +} + +RootShadowNode::Shared getCurrentShadowTreeRevision( + facebook::jsi::Runtime& runtime, + jsi::Value& nativeNodeReference) { + if (nativeNodeReference.isNumber()) { + return getCurrentShadowTreeRevision( + runtime, static_cast(nativeNodeReference.asNumber())); + } + + return getCurrentShadowTreeRevision( + runtime, getShadowNode(runtime, nativeNodeReference)->getSurfaceId()); +} + +facebook::react::PointerEventsProcessor& getPointerEventsProcessorFromRuntime( + facebook::jsi::Runtime& runtime) { + return facebook::react::UIManagerBinding::getBinding(runtime) + ->getPointerEventsProcessor(); +} + +std::vector getArrayOfInstanceHandlesFromShadowNodes( + const std::vector>& nodes, + facebook::jsi::Runtime& runtime) { + // JSI doesn't support adding elements to an array after creation, + // so we need to accumulate the values in a vector and then create + // the array when we know the size. + std::vector nonNullInstanceHandles; + nonNullInstanceHandles.reserve(nodes.size()); + for (const auto& shadowNode : nodes) { + auto instanceHandle = (*shadowNode).getInstanceHandle(runtime); + if (!instanceHandle.isNull()) { + nonNullInstanceHandles.push_back(std::move(instanceHandle)); + } + } + + return nonNullInstanceHandles; +} + +bool isRootShadowNode(const ShadowNode& shadowNode) { + return shadowNode.getTraits().check(ShadowNodeTraits::Trait::RootNodeKind); +} + +} // namespace + +#pragma mark - NativeDOM + +NativeDOM::NativeDOM(std::shared_ptr jsInvoker) + : NativeDOMCxxSpec(std::move(jsInvoker)) {} + +#pragma mark - Methods from the Node interface (for ReadOnlyNode). + +double NativeDOM::compareDocumentPosition( + jsi::Runtime& rt, + jsi::Value nativeNodeReference, + jsi::Value otherNativeNodeReference) { + auto currentRevision = getCurrentShadowTreeRevision(rt, nativeNodeReference); + if (currentRevision == nullptr) { + return dom::DOCUMENT_POSITION_DISCONNECTED; + } + + std::shared_ptr shadowNode; + std::shared_ptr otherShadowNode; + + // Check if document references are used + if (nativeNodeReference.isNumber() || otherNativeNodeReference.isNumber()) { + if (nativeNodeReference.isNumber() && otherNativeNodeReference.isNumber()) { + // Both are documents (and equality is handled in JS directly). + return dom::DOCUMENT_POSITION_DISCONNECTED; + } else if (nativeNodeReference.isNumber()) { + // Only the first is a document + auto surfaceId = nativeNodeReference.asNumber(); + shadowNode = currentRevision; + otherShadowNode = getShadowNode(rt, otherNativeNodeReference); + + if (isRootShadowNode(*otherShadowNode)) { + // If the other is a root node, we just need to check if it is its + // `documentElement` + return (surfaceId == otherShadowNode->getSurfaceId()) + ? dom::DOCUMENT_POSITION_CONTAINED_BY | + dom::DOCUMENT_POSITION_FOLLOWING + : dom::DOCUMENT_POSITION_DISCONNECTED; + } else { + // Otherwise, we'll use the root node to represent the document + // (the result should be the same) + } + } else { + // Only the second is a document + auto otherSurfaceId = otherNativeNodeReference.asNumber(); + shadowNode = getShadowNode(rt, nativeNodeReference); + otherShadowNode = getCurrentShadowTreeRevision(rt, otherSurfaceId); + + if (isRootShadowNode(*shadowNode)) { + // If this is a root node, we just need to check if the other is its + // document. + return (otherSurfaceId == shadowNode->getSurfaceId()) + ? dom::DOCUMENT_POSITION_CONTAINS | dom::DOCUMENT_POSITION_PRECEDING + : dom::DOCUMENT_POSITION_DISCONNECTED; + } else { + // Otherwise, we'll use the root node to represent the document + // (the result should be the same) + } + } + } else { + shadowNode = getShadowNode(rt, nativeNodeReference); + otherShadowNode = getShadowNode(rt, otherNativeNodeReference); + } + + return dom::compareDocumentPosition( + currentRevision, *shadowNode, *otherShadowNode); +} + +std::vector NativeDOM::getChildNodes( + jsi::Runtime& rt, + jsi::Value nativeNodeReference) { + auto currentRevision = getCurrentShadowTreeRevision(rt, nativeNodeReference); + if (currentRevision == nullptr) { + return std::vector{}; + } + + // The only child node of the document is the root node. + if (nativeNodeReference.isNumber()) { + return getArrayOfInstanceHandlesFromShadowNodes({currentRevision}, rt); + } + + auto childNodes = dom::getChildNodes( + currentRevision, *getShadowNode(rt, nativeNodeReference)); + return getArrayOfInstanceHandlesFromShadowNodes(childNodes, rt); +} + +jsi::Value NativeDOM::getElementById( + jsi::Runtime& rt, + SurfaceId surfaceId, + const std::string& id) { + auto currentRevision = getCurrentShadowTreeRevision(rt, surfaceId); + if (currentRevision == nullptr) { + return jsi::Value::undefined(); + } + + auto elementById = dom::getElementById(currentRevision, id); + if (elementById == nullptr) { + return jsi::Value::undefined(); + } + + return elementById->getInstanceHandle(rt); +} + +jsi::Value NativeDOM::getParentNode( + jsi::Runtime& rt, + jsi::Value nativeNodeReference) { + // The document does not have a parent node. + if (nativeNodeReference.isNumber()) { + return jsi::Value::undefined(); + } + + auto shadowNode = getShadowNode(rt, nativeNodeReference); + + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return jsi::Value::undefined(); + } + + // The parent of the surface's root node is the document. Only the actual + // root node qualifies: nested nodes that carry the `RootNodeKind` trait + // (e.g. , portals/overlays) still have a real parent in the shadow + // tree and must report it. Otherwise capture/bubble event propagation is + // silently severed at that boundary (a listener on an ancestor rendered + // above the modal would never receive descendant focus/blur, etc.). + if (ShadowNode::sameFamily(*currentRevision, *shadowNode)) { + return jsi::Value{shadowNode->getSurfaceId()}; + } + + auto parentShadowNode = dom::getParentNode(currentRevision, *shadowNode); + if (parentShadowNode == nullptr) { + return jsi::Value::undefined(); + } + + return parentShadowNode->getInstanceHandle(rt); +} + +bool NativeDOM::isConnected(jsi::Runtime& rt, jsi::Value nativeNodeReference) { + auto currentRevision = getCurrentShadowTreeRevision(rt, nativeNodeReference); + if (currentRevision == nullptr) { + return false; + } + + // The document is connected because we got a value for current revision. + if (nativeNodeReference.isNumber()) { + return true; + } + + auto shadowNode = getShadowNode(rt, nativeNodeReference); + return dom::isConnected(currentRevision, *shadowNode); +} + +#pragma mark - Methods from the Element interface (for ReactNativeElement). + +std::tuple< + /* topWidth: */ int, + /* rightWidth: */ int, + /* bottomWidth: */ int, + /* leftWidth: */ int> +NativeDOM::getBorderWidth( + jsi::Runtime& rt, + std::shared_ptr shadowNode) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return {0, 0, 0, 0}; + } + + auto borderWidth = dom::getBorderWidth(currentRevision, *shadowNode); + return std::tuple{ + borderWidth.top, borderWidth.right, borderWidth.bottom, borderWidth.left}; +} + +std::tuple< + /* x: */ double, + /* y: */ double, + /* width: */ double, + /* height: */ double> +NativeDOM::getBoundingClientRect( + jsi::Runtime& rt, + std::shared_ptr shadowNode, + bool includeTransform) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return {0, 0, 0, 0}; + } + + auto domRect = dom::getBoundingClientRect( + currentRevision, *shadowNode, includeTransform); + + return std::tuple{domRect.x, domRect.y, domRect.width, domRect.height}; +} + +std::tuple NativeDOM::getInnerSize( + jsi::Runtime& rt, + std::shared_ptr shadowNode) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return {0, 0}; + } + + auto innerSize = dom::getInnerSize(currentRevision, *shadowNode); + return std::tuple{innerSize.width, innerSize.height}; +} + +std::tuple +NativeDOM::getScrollPosition( + jsi::Runtime& rt, + std::shared_ptr shadowNode) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return {0, 0}; + } + + auto domPoint = dom::getScrollPosition(currentRevision, *shadowNode); + return std::tuple{domPoint.x, domPoint.y}; +} + +std::tuple +NativeDOM::getScrollSize( + jsi::Runtime& rt, + std::shared_ptr shadowNode) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return {0, 0}; + } + + auto scrollSize = dom::getScrollSize(currentRevision, *shadowNode); + return std::tuple{scrollSize.width, scrollSize.height}; +} + +std::string NativeDOM::getTagName( + jsi::Runtime& /*rt*/, + std::shared_ptr shadowNode) { + return dom::getTagName(*shadowNode); +} + +std::string NativeDOM::getTextContent( + jsi::Runtime& rt, + std::shared_ptr shadowNode) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return ""; + } + + return dom::getTextContent(currentRevision, *shadowNode); +} + +bool NativeDOM::hasPointerCapture( + jsi::Runtime& rt, + std::shared_ptr shadowNode, + double pointerId) { + bool isCapturing = getPointerEventsProcessorFromRuntime(rt).hasPointerCapture( + static_cast(pointerId), shadowNode.get()); + return isCapturing; +} + +void NativeDOM::releasePointerCapture( + jsi::Runtime& rt, + std::shared_ptr shadowNode, + double pointerId) { + getPointerEventsProcessorFromRuntime(rt).releasePointerCapture( + static_cast(pointerId), shadowNode.get()); +} + +void NativeDOM::setPointerCapture( + jsi::Runtime& rt, + std::shared_ptr shadowNode, + double pointerId) { + getPointerEventsProcessorFromRuntime(rt).setPointerCapture( + static_cast(pointerId), shadowNode); +} + +#pragma mark - Methods from the HTMLElement interface (for ReactNativeElement). + +std::tuple< + /* offsetParent: */ jsi::Value, + /* top: */ double, + /* left: */ double> +NativeDOM::getOffset( + jsi::Runtime& rt, + std::shared_ptr shadowNode) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return {jsi::Value::undefined(), 0, 0}; + } + + auto domOffset = dom::getOffset(currentRevision, *shadowNode); + + return std::tuple{ + domOffset.offsetParent == nullptr + ? jsi::Value::undefined() + : domOffset.offsetParent->getInstanceHandle(rt), + domOffset.top, + domOffset.left}; +} + +#pragma mark - Special methods to handle the root node. + +jsi::Value NativeDOM::linkRootNode( + jsi::Runtime& rt, + SurfaceId surfaceId, + jsi::Value instanceHandle) { + auto currentRevision = getCurrentShadowTreeRevision(rt, surfaceId); + if (currentRevision == nullptr) { + return jsi::Value::undefined(); + } + + auto instanceHandleWrapper = + std::make_shared(rt, instanceHandle, surfaceId); + currentRevision->setInstanceHandle(instanceHandleWrapper); + + return Bridging>::toJs(rt, currentRevision); +} + +#pragma mark - Legacy layout APIs (for ReactNativeElement). + +void NativeDOM::measure( + jsi::Runtime& rt, + std::shared_ptr shadowNode, + const MeasureOnSuccessCallback& callback) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + callback(0, 0, 0, 0, 0, 0); + return; + } + + auto measureRect = dom::measure(currentRevision, *shadowNode); + + callback( + measureRect.x, + measureRect.y, + measureRect.width, + measureRect.height, + measureRect.pageX, + measureRect.pageY); +} + +void NativeDOM::measureInWindow( + jsi::Runtime& rt, + std::shared_ptr shadowNode, + const MeasureInWindowOnSuccessCallback& callback) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + callback(0, 0, 0, 0); + return; + } + + auto rect = dom::measureInWindow(currentRevision, *shadowNode); + callback(rect.x, rect.y, rect.width, rect.height); +} + +void NativeDOM::measureLayout( + jsi::Runtime& rt, + std::shared_ptr shadowNode, + std::shared_ptr relativeToShadowNode, + jsi::Function onFail, + const MeasureLayoutOnSuccessCallback& onSuccess) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + onFail.call(rt); + return; + } + + auto maybeRect = + dom::measureLayout(currentRevision, *shadowNode, *relativeToShadowNode); + + if (!maybeRect) { + onFail.call(rt); + return; + } + + auto rect = maybeRect.value(); + + onSuccess(rect.x, rect.y, rect.width, rect.height); +} + +#pragma mark - Legacy direct manipulation APIs (for ReactNativeElement). + +void NativeDOM::setNativeProps( + jsi::Runtime& rt, + std::shared_ptr shadowNode, + jsi::Value updatePayload) { + getUIManagerFromRuntime(rt).setNativeProps_DEPRECATED( + shadowNode, RawProps(rt, updatePayload)); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/nativemodule/dom/NativeDOM.h b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/nativemodule/dom/NativeDOM.h new file mode 100644 index 0000000000..78937e375f --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/nativemodule/dom/NativeDOM.h @@ -0,0 +1,116 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#if __has_include("FBReactNativeSpecJSI.h") // CocoaPod headers on Apple +#include "FBReactNativeSpecJSI.h" +#else +#include +#endif + +#include +#include +#include + +namespace facebook::react { + +using MeasureOnSuccessCallback = SyncCallback; + +using MeasureInWindowOnSuccessCallback = SyncCallback; + +using MeasureLayoutOnSuccessCallback = SyncCallback; + +class NativeDOM : public NativeDOMCxxSpec { + public: + NativeDOM(std::shared_ptr jsInvoker); + +#pragma mark - Methods from the Node interface (for ReadOnlyNode). + + double compareDocumentPosition(jsi::Runtime &rt, jsi::Value nativeNodeReference, jsi::Value otherNativeNodeReference); + + std::vector getChildNodes(jsi::Runtime &rt, jsi::Value nativeNodeReference); + + jsi::Value getElementById(jsi::Runtime &rt, SurfaceId surfaceId, const std::string &id); + + jsi::Value getParentNode(jsi::Runtime &rt, jsi::Value nativeNodeReference); + + bool isConnected(jsi::Runtime &rt, jsi::Value nativeNodeReference); + +#pragma mark - Methods from the Element interface (for ReactNativeElement). + + std::tuple< + /* topWidth: */ int, + /* rightWidth: */ int, + /* bottomWidth: */ int, + /* leftWidth: */ int> getBorderWidth(jsi::Runtime &rt, std::shared_ptr shadowNode); + + std::tuple< + /* x: */ double, + /* y: */ double, + /* width: */ double, + /* height: */ double> + getBoundingClientRect(jsi::Runtime &rt, std::shared_ptr shadowNode, bool includeTransform); + + std::tuple getInnerSize( + jsi::Runtime &rt, + std::shared_ptr shadowNode); + + std::tuple getScrollPosition( + jsi::Runtime &rt, + std::shared_ptr shadowNode); + + std::tuple getScrollSize( + jsi::Runtime &rt, + std::shared_ptr shadowNode); + + std::string getTagName(jsi::Runtime &rt, std::shared_ptr shadowNode); + + std::string getTextContent(jsi::Runtime &rt, std::shared_ptr shadowNode); + + bool hasPointerCapture(jsi::Runtime &rt, std::shared_ptr shadowNode, double pointerId); + + void releasePointerCapture(jsi::Runtime &rt, std::shared_ptr shadowNode, double pointerId); + + void setPointerCapture(jsi::Runtime &rt, std::shared_ptr shadowNode, double pointerId); + +#pragma mark - Methods from the HTMLElement interface (for ReactNativeElement). + + std::tuple< + /* offsetParent: */ jsi::Value, + /* top: */ double, + /* left: */ double> getOffset(jsi::Runtime &rt, std::shared_ptr shadowNode); + +#pragma mark - Special methods to handle the root node. + + jsi::Value linkRootNode(jsi::Runtime &rt, SurfaceId surfaceId, jsi::Value instanceHandle); + +#pragma mark - Legacy layout APIs (for ReactNativeElement). + + void + measure(jsi::Runtime &rt, std::shared_ptr shadowNode, const MeasureOnSuccessCallback &callback); + + void measureInWindow( + jsi::Runtime &rt, + std::shared_ptr shadowNode, + const MeasureInWindowOnSuccessCallback &callback); + + void measureLayout( + jsi::Runtime &rt, + std::shared_ptr shadowNode, + std::shared_ptr relativeToShadowNode, + jsi::Function onFail, + const MeasureLayoutOnSuccessCallback &onSuccess); + +#pragma mark - Legacy direct manipulation APIs (for ReactNativeElement). + + void setNativeProps(jsi::Runtime &rt, std::shared_ptr shadowNode, jsi::Value updatePayload); +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/textinput/BaseTextInputShadowNode.h b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/textinput/BaseTextInputShadowNode.h new file mode 100644 index 0000000000..e6db7cbdc2 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/textinput/BaseTextInputShadowNode.h @@ -0,0 +1,239 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +inline ShadowView shadowViewFromShadowNode(const ShadowNode &shadowNode) +{ + auto shadowView = ShadowView{shadowNode}; + // Clearing `props` and `state` (which we don't use) allows avoiding retain + // cycles. + shadowView.props = nullptr; + shadowView.state = nullptr; + return shadowView; +} + +/* + * Base `ShadowNode` for component. + */ +template +class BaseTextInputShadowNode + : public ConcreteViewShadowNode, + public BaseTextShadowNode { + public: + using BaseShadowNode = ConcreteViewShadowNode; + + using BaseShadowNode::ConcreteViewShadowNode; + + static ShadowNodeTraits BaseTraits() + { + auto traits = BaseShadowNode::BaseTraits(); + traits.set(ShadowNodeTraits::Trait::LeafYogaNode); + traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode); + traits.set(ShadowNodeTraits::Trait::BaselineYogaNode); + return traits; + } + + /* + * Associates a shared `TextLayoutManager` with the node. + * `TextInputShadowNode` uses the manager to measure text content + * and construct `TextInputState` objects. + */ + void setTextLayoutManager(std::shared_ptr textLayoutManager) + { + Sealable::ensureUnsealed(); + textLayoutManager_ = std::move(textLayoutManager); + } + + protected: + Size measureContent(const LayoutContext &layoutContext, const LayoutConstraints &layoutConstraints) const override + { + const auto &props = BaseShadowNode::getConcreteProps(); + auto textConstraints = getTextConstraints(layoutConstraints); + + TextLayoutContext textLayoutContext{ + .pointScaleFactor = layoutContext.pointScaleFactor, + .surfaceId = BaseShadowNode::getSurfaceId(), + }; + auto textSize = textLayoutManager_ + ->measure( + attributedStringBoxToMeasure(layoutContext), + props.paragraphAttributes, + textLayoutContext, + textConstraints) + .size; + + return layoutConstraints.clamp(textSize); + } + + void layout(LayoutContext layoutContext) override + { + updateStateIfNeeded(layoutContext); + BaseShadowNode::layout(layoutContext); + } + + Float baseline(const LayoutContext &layoutContext, Size size) const override + { + const auto &props = BaseShadowNode::getConcreteProps(); + auto attributedString = getAttributedString(layoutContext); + + if (attributedString.isEmpty()) { + attributedString = getPlaceholderAttributedString(layoutContext); + } + + // Yoga expects a baseline relative to the Node's border-box edge instead of + // the content, so we need to adjust by the padding and border widths, which + // have already been set by the time of baseline alignment + auto top = YGNodeLayoutGetBorder(&this->yogaNode_, YGEdgeTop) + + YGNodeLayoutGetPadding(&this->yogaNode_, YGEdgeTop); + + AttributedStringBox attributedStringBox{attributedString}; + + if constexpr (TextLayoutManagerExtended::supportsLineMeasurement()) { + auto lines = TextLayoutManagerExtended(*textLayoutManager_) + .measureLines(attributedStringBox, props.paragraphAttributes, size); + return LineMeasurement::baseline(lines) + top; + } else { + LOG(WARNING) << "Baseline alignment is not supported by the current platform"; + return top; + } + } + + /* + * Determines the constraints to use while measure the underlying text + */ + LayoutConstraints getTextConstraints(const LayoutConstraints &layoutConstraints) const + { + if (BaseShadowNode::getConcreteProps().multiline) { + return layoutConstraints; + } else { + // A single line TextInput acts as a horizontal scroller of infinitely + // expandable text, so we want to measure the text as if it is allowed to + // infinitely expand horizontally, and later clamp to the constraints of + // the input. + return LayoutConstraints{ + .minimumSize = layoutConstraints.minimumSize, + .maximumSize = + Size{ + .width = std::numeric_limits::infinity(), + .height = layoutConstraints.maximumSize.height, + }, + .layoutDirection = layoutConstraints.layoutDirection, + }; + } + } + + std::shared_ptr textLayoutManager_; + + private: + /* + * Creates a `State` object if needed. + */ + void updateStateIfNeeded(const LayoutContext &layoutContext) + { + Sealable::ensureUnsealed(); + const auto &stateData = BaseShadowNode::getStateData(); + const auto &reactTreeAttributedString = getAttributedString(layoutContext); + + if (stateData.reactTreeAttributedString.isContentEqual(reactTreeAttributedString)) { + return; + } + + const auto &props = BaseShadowNode::getConcreteProps(); + BaseShadowNode::setStateData( + TextInputState{ + AttributedStringBox{reactTreeAttributedString}, + reactTreeAttributedString, + props.paragraphAttributes, + props.mostRecentEventCount}); + } + + /* + * Returns a `AttributedString` which represents text content of the node. + */ + AttributedString getAttributedString(const LayoutContext &layoutContext) const + { + const auto &props = BaseShadowNode::getConcreteProps(); + const auto textAttributes = props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier); + + AttributedString attributedString; + attributedString.appendFragment( + AttributedString::Fragment{ + .string = props.text, + .textAttributes = textAttributes, + .parentShadowView = shadowViewFromShadowNode(*this)}); + + auto attachments = BaseTextShadowNode::Attachments{}; + BaseTextShadowNode::buildAttributedString(textAttributes, *this, attributedString, attachments); + attributedString.setBaseTextAttributes(textAttributes); + return attributedString; + } + + /* + * Returns an `AttributedStringBox` which represents text content that should + * be used for measuring purposes. It might contain actual text value, + * placeholder value or some character that represents the size of the font. + */ + AttributedStringBox attributedStringBoxToMeasure(const LayoutContext &layoutContext) const + { + bool meaningfulState = BaseShadowNode::getState() && + BaseShadowNode::getState()->getRevision() != State::initialRevisionValue && + BaseShadowNode::getStateData().reactTreeAttributedString.getBaseTextAttributes().fontSizeMultiplier == + layoutContext.fontSizeMultiplier; + if (meaningfulState) { + const auto &stateData = BaseShadowNode::getStateData(); + auto attributedStringBox = stateData.attributedStringBox; + if (attributedStringBox.getMode() == AttributedStringBox::Mode::OpaquePointer || + !attributedStringBox.getValue().isEmpty()) { + return stateData.attributedStringBox; + } + } + + auto attributedString = meaningfulState ? AttributedString{} : getAttributedString(layoutContext); + + if (attributedString.isEmpty()) { + attributedString = getPlaceholderAttributedString(layoutContext); + } + return AttributedStringBox{attributedString}; + } + + AttributedString getPlaceholderAttributedString(const LayoutContext &layoutContext) const + { + const auto &props = BaseShadowNode::getConcreteProps(); + + AttributedString attributedString; + attributedString.setBaseTextAttributes(props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier)); + + if (!props.placeholder.empty()) { + attributedString.appendFragment( + {.string = props.placeholder, + .textAttributes = attributedString.getBaseTextAttributes(), + .parentShadowView = {}}); + } + return attributedString; + } +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/view/HostPlatformViewProps.h b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/view/HostPlatformViewProps.h new file mode 100644 index 0000000000..28a49d834d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/view/HostPlatformViewProps.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include + +namespace facebook::react { +class HostPlatformViewProps : public BaseViewProps { + public: + HostPlatformViewProps() = default; + HostPlatformViewProps( + const PropsParserContext& context, + const HostPlatformViewProps& sourceProps, + const RawProps& rawProps) + : BaseViewProps(context, sourceProps, rawProps), + focusable(convertRawProp( + context, + rawProps, + "focusable", + sourceProps.focusable, + false)), + desktopDragRegion(convertRawProp( + context, + rawProps, + "desktopDragRegion", + sourceProps.desktopDragRegion, + false)), + // Either handler is enough to make the view a context-menu target. Fabric + // turns a function prop into `true`, so a view with only the capture + // handler used to get no listener at all. + onContextMenu( + convertRawProp( + context, + rawProps, + "onContextMenu", + sourceProps.onContextMenu, + false) || + convertRawProp( + context, + rawProps, + "onContextMenuCapture", + false, + false)) {} + + void setProp( + const PropsParserContext& context, + RawPropsPropNameHash hash, + const char* propName, + const RawValue& value) { + BaseViewProps::setProp(context, hash, propName, value); + static auto defaults = HostPlatformViewProps{}; + switch (hash) { + RAW_SET_PROP_SWITCH_CASE_BASIC(focusable); + RAW_SET_PROP_SWITCH_CASE_BASIC(desktopDragRegion); + // There is one stored flag, so only `onContextMenu` gets a case here; a + // second case would need a field that does not exist. An update to either + // prop still re-parses through the constructor above, where the two are + // OR'd, so a view that swaps `onContextMenu` for `onContextMenuCapture` + // keeps its listener. + RAW_SET_PROP_SWITCH_CASE_BASIC(onContextMenu); + } + } + + bool focusable{false}; + bool desktopDragRegion{false}; + /// True when the view has an `onContextMenu` handler (RN turns a function + /// prop into this boolean marker). + bool onContextMenu{false}; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/view/HostPlatformViewTraitsInitializer.h b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/view/HostPlatformViewTraitsInitializer.h new file mode 100644 index 0000000000..95c9a888ef --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/components/view/HostPlatformViewTraitsInitializer.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace facebook::react::HostPlatformViewTraitsInitializer { +inline bool formsStackingContext(const ViewProps & /*props*/) +{ + return true; +} + +inline bool formsView(const ViewProps & /*props*/) +{ + return false; +} + +inline bool isKeyboardFocusable(const ViewProps &props) +{ + return props.focusable; +} + +} // namespace facebook::react::HostPlatformViewTraitsInitializer diff --git a/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/textlayoutmanager/platform/cxx/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/textlayoutmanager/platform/cxx/react/renderer/textlayoutmanager/TextLayoutManager.cpp new file mode 100644 index 0000000000..b4da4d0e1a --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCommonOverrides/react/renderer/textlayoutmanager/platform/cxx/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Upstream's cxx `TextLayoutManager::measure` returns the Yoga minimum size; +// this one measures through the table in GpuiTextMeasure.h. + +// This file is compiled from ReactCommonOverrides/, not from next to its +// header, so the include is spelled relative to an include root +// (`${platform_DIR}` = .../textlayoutmanager/platform/cxx/). +#include + +#include "GpuiTextMeasure.h" // rngpui::gpuiTextMeasureTable + +namespace rngpui { +namespace { +GpuiTextMeasureTable gTextMeasureTable{}; +} // namespace + +void setGpuiTextMeasureTable(const GpuiTextMeasureTable& table) { + gTextMeasureTable = table; +} + +const GpuiTextMeasureTable* gpuiTextMeasureTable() { + return gTextMeasureTable.measureParagraph ? &gTextMeasureTable : nullptr; +} + +} // namespace rngpui + +namespace facebook::react { +TextLayoutManager::TextLayoutManager( + const std::shared_ptr& contextContainer) + : contextContainer_(contextContainer), textMeasureCache_(kSimpleThreadSafeCacheSizeCap) {} + +TextMeasurement TextLayoutManager::measure( + const AttributedStringBox& attributedStringBox, + const ParagraphAttributes& paragraphAttributes, + const TextLayoutContext& /*layoutContext*/, + const LayoutConstraints& layoutConstraints) const { + const auto& fragments = attributedStringBox.getValue().getFragments(); + + TextMeasurement::Attachments attachments; + for (const auto& fragment : fragments) { + if (fragment.isAttachment()) { + attachments.push_back( + TextMeasurement::Attachment{ + .frame = {.origin = {.x = 0, .y = 0}, .size = {.width = 0, .height = 0}}, + .isClipped = false}); + } + } + + // Without a table (GpuiHost not started yet) measure to zero, the same + // result the Rust side returns when no measure handler is registered. + rngpui::GpuiTextSize size{.width = 0, .height = 0}; + if (const auto* table = rngpui::gpuiTextMeasureTable()) { + size = table->measureParagraph( + attributedStringBox.getValue(), + paragraphAttributes, + layoutConstraints.maximumSize.width, + layoutConstraints.maximumSize.height); + } + + return TextMeasurement{ + .size = layoutConstraints.clamp({.width = size.width, .height = size.height}), + .attachments = std::move(attachments)}; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/AppStateModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/AppStateModule.cpp new file mode 100644 index 0000000000..9b94000636 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/AppStateModule.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "AppStateModule.h" + +namespace facebook::react { + +AppStateConstants AppStateModule::getConstants(jsi::Runtime& /*rt*/) { + // TODO: T160890586 Wire up AppState implementation to use real data + return AppStateConstants{.initialAppState = ""}; +} + +void AppStateModule::getCurrentAppState( + jsi::Runtime& /*rt*/, + const AsyncCallback& /*success*/, + jsi::Function /*error*/) {} + +void AppStateModule::addListener( + jsi::Runtime& /*rt*/, + const std::string& /*eventName*/) {} + +void AppStateModule::removeListeners(jsi::Runtime& /*rt*/, double /*count*/) {} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/AppStateModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/AppStateModule.h new file mode 100644 index 0000000000..089cd19c49 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/AppStateModule.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +using AppStateConstants = NativeAppStateAppStateConstants; + +template <> +struct Bridging : NativeAppStateAppStateConstantsBridging {}; + +using AppState = NativeAppStateAppState; + +template <> +struct Bridging : NativeAppStateAppStateBridging {}; + +class AppStateModule : public NativeAppStateCxxSpec { + public: + explicit AppStateModule(std::shared_ptr jsInvoker) : NativeAppStateCxxSpec(jsInvoker) {} + + AppStateConstants getConstants(jsi::Runtime &rt); + + void getCurrentAppState(jsi::Runtime &rt, const AsyncCallback &success, jsi::Function error); + void addListener(jsi::Runtime &rt, const std::string &eventName); + + void removeListeners(jsi::Runtime &rt, double count); +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/CMakeLists.txt new file mode 100644 index 0000000000..401bb8616a --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/CMakeLists.txt @@ -0,0 +1,23 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_coremodules_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_cxx_platform_react_coremodules OBJECT ${react_cxx_platform_react_coremodules_SRC}) + +target_include_directories(react_cxx_platform_react_coremodules PUBLIC ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_coremodules + folly_runtime + jsi + react_nativemodule_core + react_codegen_rncore +) +target_compile_reactnative_options(react_cxx_platform_react_coremodules PRIVATE) +target_compile_options(react_cxx_platform_react_coremodules PRIVATE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/DeviceInfoModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/DeviceInfoModule.cpp new file mode 100644 index 0000000000..c3ddf77917 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/DeviceInfoModule.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "DeviceInfoModule.h" + +#include + +namespace facebook::react { + +namespace { +std::atomic windowWidth{1280.0}; +std::atomic windowHeight{720.0}; +std::atomic displayScale{1.0}; +} // namespace + +bool DeviceInfoModule::setWindowMetrics(double width, double height, double scale) { + const auto validScale = scale > 0.0 ? scale : 1.0; + const auto oldWidth = windowWidth.exchange(width, std::memory_order_relaxed); + const auto oldHeight = windowHeight.exchange(height, std::memory_order_relaxed); + const auto oldScale = displayScale.exchange(validScale, std::memory_order_relaxed); + return oldWidth != width || oldHeight != height || oldScale != validScale; +} + +Size DeviceInfoModule::windowSize() { + return Size{ + .width = static_cast(windowWidth.load(std::memory_order_relaxed)), + .height = static_cast(windowHeight.load(std::memory_order_relaxed)), + }; +} + +DeviceInfoConstants DeviceInfoModule::getConstants(jsi::Runtime& /*rt*/) { + const auto scale = displayScale.load(std::memory_order_relaxed); + return DeviceInfoConstants{ + .Dimensions = + {.window = + DisplayMetrics{ + windowWidth.load(std::memory_order_relaxed), + windowHeight.load(std::memory_order_relaxed), + scale, + 1.0, + }}, + }; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h new file mode 100644 index 0000000000..dfd73ac9d1 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/DeviceInfoModule.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +using DisplayMetrics = NativeDeviceInfoDisplayMetrics; + +using DisplayMetricsAndroid = NativeDeviceInfoDisplayMetricsAndroid; + +using DimensionsPayload = NativeDeviceInfoDimensionsPayload< + std::optional, + std::optional, + std::optional, + std::optional>; + +using DeviceInfoConstants = + NativeDeviceInfoDeviceInfoConstants, std::optional>; + +template <> +struct Bridging : NativeDeviceInfoDisplayMetricsBridging {}; + +template <> +struct Bridging : NativeDeviceInfoDisplayMetricsAndroidBridging {}; + +template <> +struct Bridging : NativeDeviceInfoDimensionsPayloadBridging {}; + +template <> +struct Bridging : NativeDeviceInfoDeviceInfoConstantsBridging {}; + +class DeviceInfoModule : public NativeDeviceInfoCxxSpec { + public: + explicit DeviceInfoModule(std::shared_ptr jsInvoker) : NativeDeviceInfoCxxSpec(jsInvoker) {} + + static bool setWindowMetrics(double width, double height, double scale); + static Size windowSize(); + + DeviceInfoConstants getConstants(jsi::Runtime &rt); +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/PlatformConstantsModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/PlatformConstantsModule.cpp new file mode 100644 index 0000000000..196a4e3a8d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/PlatformConstantsModule.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "PlatformConstantsModule.h" + +#include "GpuiJsEngine.h" + +namespace facebook::react { + +jsi::Object PlatformConstantsModule::getConstants(jsi::Runtime& rt) { + const PlatformConstantsAndroid constants{ + .isTesting = false, + .isDisableAnimations = false, + .reactNativeVersion = + { + .major = 1000, + .minor = 0, + .patch = 0, + .prerelease = std::nullopt, + }, + .Version = 33, // Android 13 (API level 33) + .Release = "", + .Fingerprint = "", + .Model = "", + .ServerHost = "", + .uiMode = "", + .Brand = "", + .Manufacturer = "", + }; + auto result = bridging::toJs(rt, constants, jsInvoker_); + + // `Platform.constants.jsEngine`, reported by the active engine package. An + // engine without a version still reports its name. + const auto& engine = rngpui::activeJsEngine().info; + auto jsEngine = jsi::Object(rt); + jsEngine.setProperty(rt, "name", jsi::String::createFromUtf8(rt, engine.name)); + jsEngine.setProperty( + rt, "version", jsi::String::createFromUtf8(rt, engine.version)); + result.setProperty(rt, "jsEngine", std::move(jsEngine)); + return result; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/PlatformConstantsModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/PlatformConstantsModule.h new file mode 100644 index 0000000000..c9cef78cc4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/coremodules/PlatformConstantsModule.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +using ReactNativeVersionAndroid = + NativePlatformConstantsAndroidReactNativeVersionAndroid>; + +template <> +struct Bridging + : NativePlatformConstantsAndroidReactNativeVersionAndroidBridging {}; + +using PlatformConstantsAndroid = NativePlatformConstantsAndroidPlatformConstantsAndroid< + bool, + std::optional, + ReactNativeVersionAndroid, + int, + std::string, + std::string, + std::string, + std::string, + std::optional, + std::string, + std::string, + std::string>; + +template <> +struct Bridging + : NativePlatformConstantsAndroidPlatformConstantsAndroidBridging {}; + +// T159303412: [RN] Metro: Need support for new target platform +class PlatformConstantsModule : public NativePlatformConstantsAndroidCxxSpec { + public: + explicit PlatformConstantsModule(std::shared_ptr jsInvoker) + : NativePlatformConstantsAndroidCxxSpec(jsInvoker) + { + } + + std::string getAndroidID(jsi::Runtime & /*rt*/) + { + return ""; + } + + // Returns the generated constants plus a `jsEngine: { name, version }` field + // reported by the active engine package. The generated Android constants + // struct cannot carry the extra field, so the object is built here. + jsi::Object getConstants(jsi::Runtime &rt); +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/CMakeLists.txt new file mode 100644 index 0000000000..fc695df675 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_devsupport_SRC CONFIGURE_DEPENDS + *.cpp + inspector/*.cpp) +add_library(react_cxx_platform_react_devsupport OBJECT ${react_cxx_platform_react_devsupport_SRC}) + +target_include_directories(react_cxx_platform_react_devsupport PUBLIC ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_devsupport + glog + nlohmann_json + folly_runtime + jsi + OpenSSL::Crypto + react_cxx_platform_react_http + react_cxx_platform_react_nativemodule + react_cxx_platform_react_renderer_scheduler + react_cxx_platform_react_renderer_scheduler + react_cxx_platform_react_threading + react_codegen_rncore + jsinspector + react_debug + react_renderer_graphics +) +target_compile_reactnative_options(react_cxx_platform_react_devsupport PRIVATE) +target_compile_options(react_cxx_platform_react_devsupport PRIVATE -Wpedantic -Wno-deprecated-declarations) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.cpp new file mode 100644 index 0000000000..2569842c1a --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.cpp @@ -0,0 +1,47 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "DevLoadingViewModule.h" + +namespace facebook::react { + +const int32_t DEFAULT_TEXT_COLOR = 0xFFFFFFFF; +const int32_t DEFAULT_BACKGROUND_COLOR = 0xFF2584E8; + +DevLoadingViewModule::DevLoadingViewModule( + std::shared_ptr jsInvoker, + std::weak_ptr devUIDelegate) + : NativeDevLoadingViewCxxSpec(jsInvoker), + devUIDelegate_(std::move(devUIDelegate)) {} + +DevLoadingViewModule::~DevLoadingViewModule() { + if (auto devUIDelegate = devUIDelegate_.lock()) { + devUIDelegate->hideLoadingView(); + } +} + +void DevLoadingViewModule::showMessage( + jsi::Runtime& /*rt*/, + const std::string& message, + std::optional textColor, + std::optional backgroundColor, + std::optional /*dismissButton*/) { + if (auto devUIDelegate = devUIDelegate_.lock()) { + devUIDelegate->showLoadingView( + message, + SharedColor{textColor.value_or(DEFAULT_TEXT_COLOR)}, + SharedColor{backgroundColor.value_or(DEFAULT_BACKGROUND_COLOR)}); + } +} + +void DevLoadingViewModule::hide(jsi::Runtime& /*rt*/) { + if (auto devUIDelegate = devUIDelegate_.lock()) { + devUIDelegate->hideLoadingView(); + } +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.h new file mode 100644 index 0000000000..0cedcb1837 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevLoadingViewModule.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "IDevUIDelegate.h" + +#include +#include +#include + +namespace facebook::react { + +class DevLoadingViewModule : public NativeDevLoadingViewCxxSpec { + public: + DevLoadingViewModule(std::shared_ptr jsInvoker, std::weak_ptr devUIDelegate); + + ~DevLoadingViewModule() override; + + void showMessage( + jsi::Runtime &rt, + const std::string &message, + std::optional textColor, + std::optional backgroundColor, + std::optional dismissButton); + void hide(jsi::Runtime &rt); + + private: + std::weak_ptr devUIDelegate_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevServerHelper.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevServerHelper.cpp new file mode 100644 index 0000000000..139aee91cf --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevServerHelper.cpp @@ -0,0 +1,164 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "DevServerHelper.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::string_view DEFAULT_PLATFORM = "android"; + +std::string SHA256(const std::string& input) { + std::array hash{}; + SHA256_CTX sha256; + SHA256_Init(&sha256); + SHA256_Update(&sha256, input.c_str(), input.size()); + SHA256_Final(hash.data(), &sha256); + std::stringstream ss; + for (unsigned char i : hash) { + ss << std::hex << std::setw(2) << std::setfill('0') << (int)i; + } + return ss.str(); +} + +std::string urlEscape(const std::string& str) { + std::regex pattern("[^a-zA-Z0-9._~-]"); + return std::regex_replace(str, pattern, ""); +} + +} // namespace + +namespace facebook::react { + +DevServerHelper::DevServerHelper( + std::string appId, + std::string deviceName, + std::string devServerHost, + uint32_t devServerPort, + const HttpClientFactory& httpClientFactory, + JavaScriptModuleCallback javaScriptModuleCallback) noexcept + : appId_(std::move(appId)), + deviceName_(std::move(deviceName)), + devServerHost_(std::move(devServerHost)), + devServerPort_(devServerPort), + httpClient_(httpClientFactory()), + javaScriptModuleCallback_(std::move(javaScriptModuleCallback)) { + deviceId_ = SHA256(fmt::format("{}-{}", deviceName_, appId_)); +} + +std::future DevServerHelper::downloadBundleResourceSync( + const std::string& jsBundleUrl, + DownloadProgressCallback&& downloadProgressCallback) { + auto promise = std::make_shared>(); + http::NetworkCallbacks callbacks{ + .onBody = + [jsBundleUrl, promise, downloadProgressCallback]( + std::unique_ptr ioBuf) { + std::string responseStr = ioBuf->moveToFbString().toStdString(); + promise->set_value(responseStr); + }, + .onResponseComplete = + [jsBundleUrl, promise, downloadProgressCallback]( + const std::string& error, bool timeoutError) { + if (!error.empty() || timeoutError) { + if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::FAILED); + } + std::string errorMessage = + "Failed to download JS bundle from Url: " + jsBundleUrl + + ". Error: " + (error.empty() ? "Timeout" : error); + LOG(WARNING) << errorMessage; + try { + throw std::runtime_error(errorMessage); + } catch (...) { + try { + promise->set_exception(std::current_exception()); + } catch (...) { + } + } + } else if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::FINISHED); + } + }}; + httpClient_->sendRequest(std::move(callbacks), "GET", jsBundleUrl); + if (downloadProgressCallback) { + downloadProgressCallback(DownloadProgressStatus::STARTED); + } + return promise->get_future(); +} + +std::string DevServerHelper::getInspectorUrl() const { + bool isProfilingBuild = + jsinspector_modern::InspectorFlags::getInstance().getIsProfilingBuild(); + + return fmt::format( + "ws://{}:{}/inspector/device?name={}&app={}&device={}&profiling={}", + devServerHost_, + devServerPort_, + urlEscape(deviceName_), + appId_, + deviceId_, + isProfilingBuild); +} + +std::string DevServerHelper::getBundleUrl() const { + if (sourcePath_.empty()) { + return ""; + } + + bool dev = true; + bool lazy = dev; + bool minify = false; + bool splitBundle = false; + bool modulesOnly = splitBundle; + bool runModule = !splitBundle; + return fmt::format( + "http://{}:{}/{}.bundle?platform={}&dev={}&lazy={}&minify={}&app={}&modulesOnly={}&runModule={}&inlineSourceMap=false&excludeSource=true&sourcePaths=url-server", + devServerHost_, + devServerPort_, + sourcePath_, + DEFAULT_PLATFORM, + dev, + lazy, + minify, + appId_, + modulesOnly, + runModule); +}; + +std::string DevServerHelper::getPackagerConnectionUrl() const { + return fmt::format("ws://{}:{}/message", devServerHost_, devServerPort_); +} + +void DevServerHelper::openDebugger() const { + auto requestUrl = fmt::format( + "http://{}:{}/open-debugger?device={}", + devServerHost_, + devServerPort_, + deviceId_); + httpClient_->sendRequest({}, "POST", requestUrl); +} + +void DevServerHelper::setupHMRClient() const { + folly::dynamic params = folly::dynamic::array( + DEFAULT_PLATFORM, + sourcePath_, + devServerHost_, + devServerPort_, + true /*enable*/); + javaScriptModuleCallback_("HMRClient", "setup", std::move(params)); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevServerHelper.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevServerHelper.h new file mode 100644 index 0000000000..7497f070e5 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevServerHelper.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class DevServerHelper { + public: + enum class DownloadProgressStatus : short { STARTED, FAILED, FINISHED }; + using DownloadProgressCallback = std::function; + + DevServerHelper( + std::string appId, + std::string deviceName, + std::string devServerHost, + uint32_t devServerPort, + const HttpClientFactory &httpClientFactory, + JavaScriptModuleCallback javaScriptModuleCallback) noexcept; + ~DevServerHelper() noexcept = default; + + std::future downloadBundleResourceSync( + const std::string &jsBundleUrl, + DownloadProgressCallback &&downloadProgressCallback = nullptr); + + std::string getInspectorUrl() const; + + std::string getBundleUrl() const; + + std::string getPackagerConnectionUrl() const; + + void openDebugger() const; + + void setSourcePath(const std::string &sourcePath) + { + sourcePath_ = sourcePath; + } + + void setupHMRClient() const; + + // react-native-gpui: the Dev Menu's "Configure Bundler". Every URL this + // helper hands out is built from these on demand, so the next bundle load + // and the connections made after it use the new server. + void setDevServer(std::string host, uint32_t port) + { + devServerHost_ = std::move(host); + devServerPort_ = port; + } + + const std::string &devServerHost() const + { + return devServerHost_; + } + + uint32_t devServerPort() const + { + return devServerPort_; + } + + private: + std::string appId_; + std::string deviceName_; + std::string devServerHost_; + uint32_t devServerPort_; + std::unique_ptr httpClient_; + JavaScriptModuleCallback javaScriptModuleCallback_; + std::string deviceId_; + std::string sourcePath_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevSettingsModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevSettingsModule.cpp new file mode 100644 index 0000000000..1a1242c6a1 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevSettingsModule.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "DevSettingsModule.h" + +#include + +/// Defined in `rn-gpui-sys/cpp/GpuiHost.cpp`: forwards a `DevSettings` toggle +/// to the GPUI main thread, which owns the perf monitor and the Dev Menu. +extern "C" void rn_gpui_dev_settings_changed(const char* name, bool enabled); + +namespace facebook::react { + +void DevSettingsModule::reload(jsi::Runtime& /*rt*/) { + LOG(INFO) << "DevSettingsModule::reload"; + if (liveReloadCallback_) { + liveReloadCallback_(); + } +} + +void DevSettingsModule::reloadWithReason( + jsi::Runtime& /*rt*/, + const std::string& reason) { + LOG(INFO) << "DevSettingsModule::reloadWithReason: " << reason; + if (liveReloadCallback_) { + liveReloadCallback_(); + } +} + +void DevSettingsModule::onFastRefresh(jsi::Runtime& /*rt*/) { + LOG(INFO) << "DevSettingsModule::onFastRefresh"; +} + +void DevSettingsModule::setHotLoadingEnabled( + jsi::Runtime& /*rt*/, + bool isHotLoadingEnabled) { + LOG(INFO) << "DevSettingsModule::setHotLoadingEnabled: " + << (int)isHotLoadingEnabled; + rn_gpui_dev_settings_changed("hotLoading", isHotLoadingEnabled); +} + +void DevSettingsModule::setIsDebuggingRemotely( + jsi::Runtime& /*rt*/, + bool /*isDebuggingRemotelyEnabled*/) {} + +void DevSettingsModule::setProfilingEnabled( + jsi::Runtime& /*rt*/, + bool isProfilingEnabled) { + // The Perf Monitor is the desktop's profiling surface (plan 7.6). + rn_gpui_dev_settings_changed("profiling", isProfilingEnabled); +} + +void DevSettingsModule::toggleElementInspector(jsi::Runtime& /*rt*/) { + rn_gpui_dev_settings_changed("inspectorToggle", true); +} + +void DevSettingsModule::addMenuItem( + jsi::Runtime& /*rt*/, + const std::string& /*title*/) {} + +void DevSettingsModule::setIsShakeToShowDevMenuEnabled( + jsi::Runtime& /*rt*/, + bool /*enabled*/) {} + +void DevSettingsModule::openDebugger(jsi::Runtime& /*rt*/) { + if (auto devServerHelper = devServerHelper_.lock()) { + devServerHelper->openDebugger(); + } +} + +void DevSettingsModule::addListener( + jsi::Runtime& /*rt*/, + const std::string& /*eventName*/) { + // noop +} + +void DevSettingsModule::removeListeners( + jsi::Runtime& /*rt*/, + double /*count*/) { + // noop +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevSettingsModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevSettingsModule.h new file mode 100644 index 0000000000..a9ec5cd419 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/DevSettingsModule.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class DevSettingsModule : public NativeDevSettingsCxxSpec { + using LiveReloadCallback = std::function; + + public: + DevSettingsModule( + std::shared_ptr jsInvoker, + std::weak_ptr devServerHelper, + LiveReloadCallback liveReloadCallback) + : NativeDevSettingsCxxSpec(jsInvoker), + devServerHelper_(std::move(devServerHelper)), + liveReloadCallback_(std::move(liveReloadCallback)) + { + } + + void reload(jsi::Runtime &rt); + + void reloadWithReason(jsi::Runtime &rt, const std::string &reason); + + void onFastRefresh(jsi::Runtime &rt); + + void setHotLoadingEnabled(jsi::Runtime &rt, bool isHotLoadingEnabled); + + void setIsDebuggingRemotely(jsi::Runtime &rt, bool isDebuggingRemotelyEnabled); + + void setProfilingEnabled(jsi::Runtime &rt, bool isProfilingEnabled); + + void toggleElementInspector(jsi::Runtime &rt); + + void addMenuItem(jsi::Runtime &rt, const std::string &title); + + void setIsShakeToShowDevMenuEnabled(jsi::Runtime &rt, bool enabled); + + void openDebugger(jsi::Runtime &rt); + + void addListener(jsi::Runtime &rt, const std::string &eventName); + + void removeListeners(jsi::Runtime &rt, double count); + + private: + std::weak_ptr devServerHelper_; + LiveReloadCallback liveReloadCallback_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/IDevUIDelegate.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/IDevUIDelegate.h new file mode 100644 index 0000000000..958cc19019 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/IDevUIDelegate.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +struct IDevUIDelegate { + virtual ~IDevUIDelegate() noexcept = default; + + virtual void showDownloadBundleProgress() = 0; + + virtual void hideDownloadBundleProgress() = 0; + + virtual void showLoadingView(const std::string &message, SharedColor textColor, SharedColor backgroundColor) = 0; + + virtual void hideLoadingView() = 0; + + virtual void showDebuggerOverlay(std::function &&resumeDebuggerFn) = 0; + + virtual void hideDebuggerOverlay() = 0; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/LogBoxModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/LogBoxModule.cpp new file mode 100644 index 0000000000..745fb3953c --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/LogBoxModule.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "LogBoxModule.h" + +#include + +namespace facebook::react { + +LogBoxModule::LogBoxModule( + std::shared_ptr jsInvoker, + std::shared_ptr surfaceDelegate) + : NativeLogBoxCxxSpec(jsInvoker), + surfaceDelegate_(std::move(surfaceDelegate)) { + LOG(INFO) << "LogBoxModule initialized"; + surfaceDelegate_->createContentView("LogBox"); +} + +LogBoxModule::~LogBoxModule() { + surfaceDelegate_->destroyContentView(); +} + +void LogBoxModule::show(jsi::Runtime& /*rt*/) { + LOG(INFO) << "LogBoxModule show"; + if (surfaceDelegate_->isContentViewReady() && + !surfaceDelegate_->isShowing()) { + surfaceDelegate_->show(); + } +} + +void LogBoxModule::hide(jsi::Runtime& /*rt*/) { + LOG(INFO) << "LogBoxModule hide"; + if (surfaceDelegate_->isShowing()) { + surfaceDelegate_->hide(); + } +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/LogBoxModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/LogBoxModule.h new file mode 100644 index 0000000000..0ee88e327e --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/LogBoxModule.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { +class LogBoxModule : public NativeLogBoxCxxSpec { + public: + LogBoxModule(std::shared_ptr jsInvoker, std::shared_ptr surfaceDelegate); + ~LogBoxModule() override; + + void show(jsi::Runtime &rt); + + void hide(jsi::Runtime &rt); + + private: + std::shared_ptr surfaceDelegate_; +}; +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp new file mode 100644 index 0000000000..d17bae6ce7 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "PackagerConnection.h" + +#include +#include + +namespace facebook::react { + +PackagerConnection::PackagerConnection( + WebSocketClientFactory webSocketClientFactory, + std::string packagerConnectionUrl, + LiveReloadCallback&& liveReloadCallback, + ShowDevMenuCallback&& showDevMenuCallback) + : webSocketClientFactory_(std::move(webSocketClientFactory)), + packagerConnectionUrl_(std::move(packagerConnectionUrl)), + liveReloadCallback_(std::move(liveReloadCallback)), + showDevMenuCallback_(std::move(showDevMenuCallback)) { + attemptConnection(); +} + +PackagerConnection::~PackagerConnection() noexcept { + reconnectThread_.quit(); + if (websocket_) { + websocket_->setOnClosedCallback(nullptr); + websocket_->setOnMessageCallback(nullptr); + websocket_->close("PackagerConnection destroyed"); + } +} + +void PackagerConnection::attemptConnection() { + if (websocket_) { + websocket_->setOnClosedCallback(nullptr); + websocket_->close("reconnecting"); + } + websocket_ = webSocketClientFactory_(); + websocket_->setOnMessageCallback([this](const std::string& message) { + LOG(INFO) << "Received message from packager: " << message; + auto json = nlohmann::json::parse(message, nullptr, false); + if (json.is_discarded() || json.is_null() || json["version"] != 2) { + return; + } + auto method = json["method"]; + if (method == "reload") { + liveReloadCallback_(); + } else if (method == "showDevMenu") { + showDevMenuCallback_(); + } + }); + websocket_->setOnClosedCallback([this](const std::string& reason) { + LOG(INFO) << "PackagerConnection closed: " << reason; + scheduleReconnect(); + }); + websocket_->connect( + packagerConnectionUrl_, + [this](bool success, const std::string& /*error*/) { + if (success) { + if (!isInitialConnection_) { + LOG(INFO) + << "PackagerConnection connected to Metro - triggering live reload"; + liveReloadCallback_(); + } else { + LOG(INFO) << "PackagerConnection connected to Metro"; + } + } else { + scheduleReconnect(); + } + isInitialConnection_ = false; + }); +} + +void PackagerConnection::scheduleReconnect() { + if (reconnectPending_.exchange(true)) { + return; + } + reconnectThread_.runAsync( + [this]() { + reconnectPending_ = false; + attemptConnection(); + }, + std::chrono::milliseconds(5000)); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/PackagerConnection.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/PackagerConnection.h new file mode 100644 index 0000000000..7e1596c0f2 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/PackagerConnection.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class PackagerConnection { + using LiveReloadCallback = std::function; + using ShowDevMenuCallback = std::function; + + public: + PackagerConnection( + WebSocketClientFactory webSocketClientFactory, + std::string packagerConnectionUrl, + LiveReloadCallback &&liveReloadCallback, + ShowDevMenuCallback &&showDevMenuCallback); + ~PackagerConnection() noexcept; + PackagerConnection(const PackagerConnection &other) = delete; + PackagerConnection &operator=(PackagerConnection &other) = delete; + PackagerConnection(PackagerConnection &&other) = delete; + PackagerConnection &operator=(PackagerConnection &&other) = delete; + + private: + void attemptConnection(); + void scheduleReconnect(); + + const WebSocketClientFactory webSocketClientFactory_; + const std::string packagerConnectionUrl_; + const LiveReloadCallback liveReloadCallback_; + const ShowDevMenuCallback showDevMenuCallback_; + std::unique_ptr websocket_; + std::atomic isInitialConnection_{true}; + std::atomic reconnectPending_{false}; + TaskDispatchThread reconnectThread_{"PackagerReconnect"}; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp new file mode 100644 index 0000000000..d7ccc10514 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/SourceCodeModule.cpp @@ -0,0 +1,16 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "SourceCodeModule.h" + +namespace facebook::react { + +SourceCodeConstants SourceCodeModule::getConstants(jsi::Runtime& /*rt*/) { + return SourceCodeConstants{.scriptURL = sourceURL_}; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/SourceCodeModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/SourceCodeModule.h new file mode 100644 index 0000000000..7924c5ad1b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/SourceCodeModule.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +using SourceCodeConstants = NativeSourceCodeSourceCodeConstants; + +template <> +struct Bridging : NativeSourceCodeSourceCodeConstantsBridging {}; + +class SourceCodeModule : public NativeSourceCodeCxxSpec { + public: + explicit SourceCodeModule(std::shared_ptr jsInvoker, std::string sourceURL = "") + : NativeSourceCodeCxxSpec(jsInvoker), sourceURL_(std::move(sourceURL)) + { + } + + SourceCodeConstants getConstants(jsi::Runtime &rt); + + private: + std::string sourceURL_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/Inspector.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/Inspector.cpp new file mode 100644 index 0000000000..68a82cf1df --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/Inspector.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "Inspector.h" +#include "InspectorPackagerConnectionDelegate.h" + +#include +#include + +namespace facebook::react { + +namespace { + +constexpr std::string_view INTEGRATION_NAME = "RNHOST"; + +class InspectorHostTargetDelegate + : public jsinspector_modern::HostTargetDelegate, + public std::enable_shared_from_this { + public: + InspectorHostTargetDelegate( + std::string appName, + std::string deviceName, + const HttpClientFactory& httpClientFactory, + LiveReloadCallbackFn&& liveReloadCallbackFn, + ToggleDebuggerOverlayFn&& toggleDebuggerOverlayFn) noexcept + : appName_(std::move(appName)), + deviceName_(std::move(deviceName)), + httpClient_(httpClientFactory()), + liveReloadCallbackFn_(std::move(liveReloadCallbackFn)), + toggleDebuggerOverlayFn_(std::move(toggleDebuggerOverlayFn)) {} + + jsinspector_modern::HostTargetMetadata getMetadata() override { + return { + .appIdentifier = appName_, + .deviceName = deviceName_, + .integrationName = std::string(INTEGRATION_NAME), + }; + } + + void onReload( + const jsinspector_modern::HostTargetDelegate::PageReloadRequest& + /*request*/) override { + if (liveReloadCallbackFn_ != nullptr) { + liveReloadCallbackFn_(); + } + } + + void onSetPausedInDebuggerMessage( + const jsinspector_modern::HostTargetDelegate:: + OverlaySetPausedInDebuggerMessageRequest& request) override { + if (toggleDebuggerOverlayFn_ != nullptr) { + toggleDebuggerOverlayFn_( + request.message.has_value(), [target = target_]() { + if (auto strongTarget = target.lock()) { + strongTarget->sendCommand( + jsinspector_modern::HostCommand::DebuggerResume); + } + }); + } + } + + void loadNetworkResource( + const jsinspector_modern::LoadNetworkResourceRequest& params, + jsinspector_modern::ScopedExecutor< + jsinspector_modern::NetworkRequestListener> executor) override { + http::NetworkCallbacks callbacks{ + .onResponse = + [executor]( + uint32_t responseCode, + const http::Headers& headers, + const std::string& /*responseUrl*/) { + executor([responseCode, headers]( + jsinspector_modern::NetworkRequestListener& + listener) { + facebook::react::jsinspector_modern::Headers responseHeaders; + for (const auto& [key, value] : headers) { + responseHeaders[key] = value; + } + listener.onHeaders(responseCode, responseHeaders); + }); + }, + .onBody = + [executor](std::unique_ptr ioBuf) { + executor( + [response = ioBuf->moveToFbString().toStdString()]( + jsinspector_modern::NetworkRequestListener& listener) { + listener.onData(response); + }); + }, + .onResponseComplete = + [executor, url = params.url]( + const std::string& error, bool timeout) { + if (error.empty() && !timeout) { + executor( + [](jsinspector_modern::NetworkRequestListener& listener) { + listener.onCompletion(); + }); + } else { + executor( + [errorMessage = + "Failed to download JS bundle from Url: " + url + + ". Error: " + (error.empty() ? "Timeout" : error)]( + jsinspector_modern::NetworkRequestListener& listener) { + listener.onError(errorMessage); + }); + } + }}; + httpClient_->sendRequest(std::move(callbacks), "GET", params.url); + } + + void setTarget(std::weak_ptr target) { + target_ = std::move(target); + } + + private: + std::string appName_; + std::string deviceName_; + std::unique_ptr httpClient_; + LiveReloadCallbackFn liveReloadCallbackFn_; + ToggleDebuggerOverlayFn toggleDebuggerOverlayFn_; + std::weak_ptr target_; +}; + +} // namespace + +Inspector::Inspector( + std::string appName, + std::string deviceName, + WebSocketClientFactory webSocketClientFactory, + HttpClientFactory httpClientFactory) noexcept + : appName_(std::move(appName)), + deviceName_(std::move(deviceName)), + webSocketClientFactory_(std::move(webSocketClientFactory)), + httpClientFactory_(std::move(httpClientFactory)) {} + +Inspector::~Inspector() noexcept { + if (pageId_) { + jsinspector_modern::getInspectorInstance().removePage(*pageId_); + taskDispatchThread_.runSync([weakThis = weak_from_this()]() { + if (auto strongThis = weakThis.lock()) { + strongThis->pageId_.reset(); + strongThis->target_.reset(); + } + }); + } + taskDispatchThread_.quit(); +} + +void Inspector::connectDebugger(const std::string& inspectorUrl) noexcept { + invokeElsePost([weakThis = weak_from_this(), inspectorUrl]() { + auto self = weakThis.lock(); + if (!self) { + return; + } + if (!self->packagerConnection_) { + self->packagerConnection_ = + std::make_unique( + inspectorUrl, + self->deviceName_, + self->appName_, + std::make_unique( + weakThis, self->webSocketClientFactory_)); + } + if (!self->packagerConnection_->isConnected()) { + self->packagerConnection_->connect(); + } + }); +} + +void Inspector::disconnectDebugger() noexcept { + invokeElsePost([weakThis = weak_from_this()]() { + if (auto self = weakThis.lock()) { + self->packagerConnection_.reset(); + } + }); +} + +void Inspector::ensureHostTarget( + LiveReloadCallbackFn&& liveReloadCallbackFn, + ToggleDebuggerOverlayFn&& toggleDebuggerOverlayFn) noexcept { + hostDelegate_ = std::make_shared( + appName_, + deviceName_, + httpClientFactory_, + std::move(liveReloadCallbackFn), + std::move(toggleDebuggerOverlayFn)); + target_ = jsinspector_modern::HostTarget::create( + *hostDelegate_, + [weakThis = weak_from_this()](std::function&& callback) { + if (auto strongThis = weakThis.lock()) { + strongThis->invokeElsePost(std::move(callback)); + } + }); + static_cast(*hostDelegate_).setTarget(target_); + jsinspector_modern::InspectorTargetCapabilities capabilities{ + .nativePageReloads = true}; + pageId_ = jsinspector_modern::getInspectorInstance().addPage( + std::string(INTEGRATION_NAME), + "", /*vm*/ + [weakInspectorTarget = std::weak_ptr(target_)]( + std::unique_ptr remote) + -> std::unique_ptr { + if (auto inspectorTarget = weakInspectorTarget.lock()) { + return inspectorTarget->connect(std::move(remote)); + } + // Reject the connection on destruction + return nullptr; + }, + capabilities); +} + +void Inspector::invokeElsePost( + TaskDispatchThread::TaskFn&& callback, + std::chrono::milliseconds delayMs) { + if (taskDispatchThread_.isOnThread() && + delayMs == std::chrono::milliseconds::zero()) { + callback(); + } else { + taskDispatchThread_.runAsync(std::move(callback), delayMs); + } +} + +std::shared_ptr Inspector::inspectorTarget() + const { + return target_; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/Inspector.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/Inspector.h new file mode 100644 index 0000000000..886f32a6e3 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/Inspector.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "InspectorThread.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +using ToggleDebuggerOverlayFn = std::function &&)>; + +using LiveReloadCallbackFn = std::function; + +class Inspector : public InspectorThread, public std::enable_shared_from_this { + public: + Inspector( + std::string appName, + std::string deviceName, + WebSocketClientFactory webSocketClientFactory, + HttpClientFactory httpClientFactory) noexcept; + ~Inspector() noexcept override; + Inspector(const Inspector &other) = delete; + Inspector &operator=(Inspector &other) = delete; + Inspector(Inspector &&other) = delete; + Inspector &operator=(Inspector &&other) = delete; + + void connectDebugger(const std::string &inspectorUrl) noexcept; + + // react-native-gpui: drop the connection to the dev server, so the next + // `connectDebugger` connects to the URL it is given (the server changed). + void disconnectDebugger() noexcept; + + void ensureHostTarget( + LiveReloadCallbackFn &&liveReloadCallbackFn, + ToggleDebuggerOverlayFn &&toggleDebuggerOverlayFn) noexcept; + + std::shared_ptr inspectorTarget() const; + + private: + void invokeElsePost( + TaskDispatchThread::TaskFn &&callback, + std::chrono::milliseconds delayMs = std::chrono::milliseconds::zero()) override; + + TaskDispatchThread taskDispatchThread_{"InspectorThread"}; + std::string appName_; + std::string deviceName_; + WebSocketClientFactory webSocketClientFactory_; + HttpClientFactory httpClientFactory_; + std::shared_ptr hostDelegate_; + std::shared_ptr target_; + std::optional pageId_; + std::unique_ptr packagerConnection_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.cpp new file mode 100644 index 0000000000..86dbca6a26 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "InspectorPackagerConnectionDelegate.h" + +#include +#include + +namespace facebook::react { + +InspectorPackagerConnectionDelegate::WebSocket::WebSocket( + const std::string& url, + std::weak_ptr webSocketDelegate, + std::weak_ptr inspectorThread, + const WebSocketClientFactory& webSocketClientFactory) + : webSocketDelegate_{webSocketDelegate}, + inspectorThread_(std::move(inspectorThread)) { + websocket_ = webSocketClientFactory(); + websocket_->setOnMessageCallback( + [webSocketDelegate, + inspectorThread = inspectorThread_](const std::string& message) { + auto strongInspectorThread = inspectorThread.lock(); + if (!strongInspectorThread) { + return; + } + strongInspectorThread->invokeElsePost([webSocketDelegate, message]() { + if (const auto strongDelegate = webSocketDelegate.lock()) { + strongDelegate->didReceiveMessage(message); + } + }); + }); + websocket_->setOnClosedCallback( + [webSocketDelegate, + inspectorThread = inspectorThread_](const std::string& /*message*/) { + auto strongInspectorThread = inspectorThread.lock(); + if (!strongInspectorThread) { + return; + } + strongInspectorThread->invokeElsePost([webSocketDelegate]() { + if (const auto strongDelegate = webSocketDelegate.lock()) { + strongDelegate->didClose(); + } + }); + }); + websocket_->connect( + url, + [webSocketDelegate, inspectorThread = inspectorThread_]( + bool success, const std::string& message) { + auto strongInspectorThread = inspectorThread.lock(); + if (!strongInspectorThread) { + return; + } + strongInspectorThread->invokeElsePost( + [webSocketDelegate, success, message]() { + if (auto strongDelegate = webSocketDelegate.lock()) { + if (success) { + strongDelegate->didOpen(); + } else { + // Assume the server is not running, and suppress an error log + strongDelegate->didFailWithError(ECONNREFUSED, message); + } + } + }); + }); +} + +InspectorPackagerConnectionDelegate::WebSocket::~WebSocket() { + websocket_->close("InspectorPackagerConnectionDelegate destroyed"); +}; + +void InspectorPackagerConnectionDelegate::WebSocket::send( + std::string_view message) { + if (auto strongInspectorThread = inspectorThread_.lock()) { + strongInspectorThread->invokeElsePost( + [this, message = std::string(message)]() { + if (websocket_) { + websocket_->send(message); + } + }); + } +} + +InspectorPackagerConnectionDelegate::InspectorPackagerConnectionDelegate( + std::weak_ptr inspectorThread, + WebSocketClientFactory webSocketClientFactory) noexcept + : inspectorThread_(std::move(inspectorThread)), + webSocketClientFactory_(std::move(webSocketClientFactory)) {} + +std::unique_ptr +InspectorPackagerConnectionDelegate::connectWebSocket( + const std::string& url, + std::weak_ptr delegate) { + return std::make_unique( + url, delegate, inspectorThread_, webSocketClientFactory_); +} + +void InspectorPackagerConnectionDelegate::scheduleCallback( + std::function callback, + std::chrono::milliseconds delayMs) { + if (auto inspectorThread = inspectorThread_.lock()) { + inspectorThread->invokeElsePost(std::move(callback), delayMs); + } +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.h new file mode 100644 index 0000000000..e91d5be9f6 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorPackagerConnectionDelegate.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "InspectorThread.h" + +#include +#include +#include +#include +#include + +namespace facebook::react { + +class IWebSocketClient; + +class InspectorPackagerConnectionDelegate final : public jsinspector_modern::InspectorPackagerConnectionDelegate { + class WebSocket : public jsinspector_modern::IWebSocket { + public: + WebSocket( + const std::string &url, + std::weak_ptr webSocketDelegate, + std::weak_ptr inspectorThread, + const WebSocketClientFactory &webSocketClientFactory); + ~WebSocket() override; + WebSocket(const WebSocket &other) = delete; + WebSocket &operator=(WebSocket &other) = delete; + WebSocket(WebSocket &&other) = delete; + WebSocket &operator=(WebSocket &&other) = delete; + + void send(std::string_view message) override; + + private: + std::weak_ptr webSocketDelegate_; + std::weak_ptr inspectorThread_; + std::unique_ptr websocket_; + }; + + public: + InspectorPackagerConnectionDelegate( + std::weak_ptr inspectorThread, + WebSocketClientFactory webSocketClientFactory) noexcept; + + std::unique_ptr connectWebSocket( + const std::string &url, + std::weak_ptr delegate) override; + + void scheduleCallback( + std::function callback, + std::chrono::milliseconds delayMs = std::chrono::milliseconds::zero()) override; + + private: + std::weak_ptr inspectorThread_; + WebSocketClientFactory webSocketClientFactory_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorThread.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorThread.h new file mode 100644 index 0000000000..1dbee83051 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/devsupport/inspector/InspectorThread.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class InspectorThread { + public: + InspectorThread() noexcept = default; + virtual ~InspectorThread() = default; + InspectorThread(const InspectorThread &other) = delete; + InspectorThread &operator=(InspectorThread &other) = delete; + InspectorThread(InspectorThread &&other) = delete; + InspectorThread &operator=(InspectorThread &&other) = delete; + + virtual void invokeElsePost( + TaskDispatchThread::TaskFn &&callback, + std::chrono::milliseconds delayMs = std::chrono::milliseconds(0)) = 0; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/CMakeLists.txt new file mode 100644 index 0000000000..e25095d9f5 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/CMakeLists.txt @@ -0,0 +1,20 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_http_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_cxx_platform_react_http OBJECT ${react_cxx_platform_react_http_SRC}) + +target_include_directories(react_cxx_platform_react_http PUBLIC ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_http + folly_runtime +) +target_compile_reactnative_options(react_cxx_platform_react_http PRIVATE) +target_compile_options(react_cxx_platform_react_http PRIVATE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IHttpClient.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IHttpClient.cpp new file mode 100644 index 0000000000..59774dabb4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IHttpClient.cpp @@ -0,0 +1,16 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace facebook::react { + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char HttpClientFactoryKey[] = "HttpClientFactory"; + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char DevToolsHttpClientFactoryKey[] = "DevToolsHttpClientFactory"; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IHttpClient.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IHttpClient.h new file mode 100644 index 0000000000..6249505dec --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IHttpClient.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace http { + +using Headers = std::vector>; + +struct FormDataField { + std::string fieldName; + Headers headers; + std::optional string; + std::optional uri; +}; + +struct BlobReference { + std::string blobId; + uint64_t offset{0}; + uint64_t size{0}; +}; + +using FormData = std::vector; + +struct Body { + std::optional string; + std::optional blob; + std::optional formData; + std::optional base64; + bool withCredentials{true}; +}; + +using OnUploadProgress = std::function; +using OnResponse = std::function; +using OnBody = std::function body)>; +using OnBodyIncremental = std::function body)>; +using OnBodyProgress = std::function; +using OnResponseComplete = std::function; + +struct NetworkCallbacks { + OnUploadProgress onUploadProgress{nullptr}; + OnResponse onResponse{nullptr}; + OnBody onBody{nullptr}; + OnBodyIncremental onBodyIncremental{nullptr}; + OnBodyProgress onBodyProgress{nullptr}; + OnResponseComplete onResponseComplete{nullptr}; + bool sendIncrementalUpdates{false}; + bool sendProgressUpdates{false}; +}; + +struct IRequestToken { + virtual ~IRequestToken() = default; + + virtual void cancel() noexcept = 0; +}; + +} // namespace http + +struct IHttpClient { + virtual ~IHttpClient() = default; + + virtual std::unique_ptr sendRequest( + http::NetworkCallbacks &&callback, + const std::string &method, + const std::string &url, + const http::Headers &headers = {}, + const http::Body &body = {}, + uint32_t timeout = 0, + std::optional loggingId = std::nullopt) = 0; +}; + +extern const char HttpClientFactoryKey[]; + +extern const char DevToolsHttpClientFactoryKey[]; + +using HttpClientFactory = std::function()>; + +HttpClientFactory getHttpClientFactory(); + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IWebSocketClient.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IWebSocketClient.cpp new file mode 100644 index 0000000000..838f687e37 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IWebSocketClient.cpp @@ -0,0 +1,17 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +namespace facebook::react { + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char WebSocketClientFactoryKey[] = "WebSocketClientFactory"; + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char DevToolsWebSocketClientFactoryKey[] = + "DevToolsWebSocketClientFactory"; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IWebSocketClient.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IWebSocketClient.h new file mode 100644 index 0000000000..7bbb36c766 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/IWebSocketClient.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +class IWebSocketClient { + public: + using OnConnectCallback = std::function; + using OnClosedCallback = std::function; + using OnMessageCallback = std::function; + using OnMessageFrameCallback = std::function; + using OnClosedFrameCallback = std::function; + + virtual ~IWebSocketClient() = default; + + virtual void setOnClosedCallback(OnClosedCallback &&callback) noexcept = 0; + + virtual void setOnMessageCallback(OnMessageCallback &&callback) noexcept = 0; + + virtual void setOnMessageFrameCallback(OnMessageFrameCallback &&callback) noexcept { + setOnMessageCallback([callback = std::move(callback)](const std::string &message) { + callback(message, false); + }); + } + + virtual void setOnClosedFrameCallback(OnClosedFrameCallback &&callback) noexcept { + setOnClosedCallback([callback = std::move(callback)](const std::string &reason) { + callback(1006, reason); + }); + } + + virtual void connect(const std::string &url, OnConnectCallback && = nullptr) = 0; + + virtual void close(const std::string &reason) = 0; + + virtual void close(uint16_t code, const std::string &reason) { + (void)code; + close(reason); + } + + virtual void send(const std::string &message) = 0; + + virtual void sendBinary(const std::string &bytes) { + send(bytes); + } + + virtual void ping() = 0; +}; + +extern const char WebSocketClientFactoryKey[]; + +extern const char DevToolsWebSocketClientFactoryKey[]; + +using WebSocketClientFactory = std::function()>; + +WebSocketClientFactory getWebSocketClientFactory(); + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/platform/cxx/WebSocketClient.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/platform/cxx/WebSocketClient.cpp new file mode 100644 index 0000000000..fffc15b971 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/platform/cxx/WebSocketClient.cpp @@ -0,0 +1,610 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "WebSocketClient.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__APPLE__) +#include +#endif +#if defined(_WIN32) +#include +#include +#include +#include +#endif + +namespace facebook::react { + +namespace { +std::atomic webSocketMessagesInFlight{0}; + +using PlainWebSocket = + boost::beast::websocket::stream; +using TlsWebSocket = boost::beast::websocket::stream< + boost::beast::ssl_stream>; +using AnyWebSocket = std::variant; + +#if defined(__APPLE__) +bool verifyWithMacSystemTrust( + const std::string& hostname, + X509_STORE_CTX* storeContext) { + CFMutableArrayRef certificates = + CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); + if (certificates == nullptr) { + return false; + } + + auto* chain = X509_STORE_CTX_get0_chain(storeContext); + const int certificateCount = chain == nullptr ? 0 : sk_X509_num(chain); + for (int index = 0; index < certificateCount; ++index) { + auto* certificate = sk_X509_value(chain, index); + const int encodedSize = i2d_X509(certificate, nullptr); + if (encodedSize <= 0) { + CFRelease(certificates); + return false; + } + std::vector encoded(static_cast(encodedSize)); + auto* encodedCursor = encoded.data(); + i2d_X509(certificate, &encodedCursor); + CFDataRef data = CFDataCreate( + kCFAllocatorDefault, + encoded.data(), + static_cast(encoded.size())); + SecCertificateRef secCertificate = + SecCertificateCreateWithData(kCFAllocatorDefault, data); + CFRelease(data); + if (secCertificate == nullptr) { + CFRelease(certificates); + return false; + } + CFArrayAppendValue(certificates, secCertificate); + CFRelease(secCertificate); + } + + CFStringRef host = CFStringCreateWithCString( + kCFAllocatorDefault, hostname.c_str(), kCFStringEncodingUTF8); + SecPolicyRef policy = SecPolicyCreateSSL(true, host); + CFRelease(host); + SecTrustRef trust = nullptr; + const auto createStatus = + SecTrustCreateWithCertificates(certificates, policy, &trust); + CFRelease(policy); + CFRelease(certificates); + if (createStatus != errSecSuccess || trust == nullptr) { + return false; + } + const bool trusted = SecTrustEvaluateWithError(trust, nullptr); + CFRelease(trust); + return trusted; +} +#endif + +#if defined(_WIN32) +void loadWindowsSystemRoots(boost::asio::ssl::context& context) { + const DWORD scopes[] = { + CERT_SYSTEM_STORE_CURRENT_USER, CERT_SYSTEM_STORE_LOCAL_MACHINE}; + for (const auto scope : scopes) { + HCERTSTORE store = CertOpenStore( + CERT_STORE_PROV_SYSTEM_W, + 0, + 0, + scope | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG, + L"ROOT"); + if (store == nullptr) { + continue; + } + PCCERT_CONTEXT certificateContext = nullptr; + while ((certificateContext = CertEnumCertificatesInStore( + store, certificateContext)) != nullptr) { + const unsigned char* encoded = certificateContext->pbCertEncoded; + X509* certificate = d2i_X509( + nullptr, + &encoded, + static_cast(certificateContext->cbCertEncoded)); + if (certificate != nullptr) { + X509_STORE_add_cert( + SSL_CTX_get_cert_store(context.native_handle()), certificate); + X509_free(certificate); + ERR_clear_error(); // Ignore duplicate certificates in the store. + } + } + CertCloseStore(store, 0); + } +} +#endif +} // namespace + +extern "C" uint64_t rn_gpui_websocket_messages_in_flight() noexcept { + return webSocketMessagesInFlight.load(std::memory_order_relaxed); +} + +struct WebSocketClient::Impl final : public std::enable_shared_from_this { + void onResolveCompleted( + boost::system::error_code ec, + const boost::asio::ip::tcp::resolver::results_type& results); + + void onConnectionCompleted(boost::system::error_code ec); + + void onTlsHandshakeCompleted(boost::system::error_code ec); + + void onHandshakeCompleted(boost::system::error_code ec); + + void listen(); + + void write(); + + void onConnectCallback(bool connected, const std::string& error); + void onClosed(uint16_t code, const std::string& reason); + + // Callbacks and Uri + OnConnectCallback onConnectCallback_; + OnClosedCallback onClosedCallback_; + OnMessageCallback onMessageCallback_; + OnMessageFrameCallback onMessageFrameCallback_; + OnClosedFrameCallback onClosedFrameCallback_; + std::optional uri_; + + // Boost Beast WebSocket Client + boost::asio::io_context ioContext_; + boost::asio::ip::tcp::resolver resolver_{ioContext_}; + boost::beast::multi_buffer buffer_; + boost::asio::ssl::context tlsContext_{boost::asio::ssl::context::tls_client}; + folly::Synchronized ws_{ + AnyWebSocket{PlainWebSocket{ioContext_}}}; + + // Input and Output handling + std::mutex mutexOut_; + std::queue> messagesOut_; + std::atomic isWriting_{false}; + std::atomic isClosing_{false}; + std::atomic didReportClosed_{false}; +}; + +WebSocketClient::WebSocketClient() noexcept + : impl_(std::make_shared()) {} + +WebSocketClient::~WebSocketClient() { + WebSocketClient::close("Force close as WebSocketClient being destroyed"); +} + +void WebSocketClient::setOnClosedCallback( + OnClosedCallback&& callback) noexcept { + impl_->onClosedCallback_ = std::move(callback); +} + +void WebSocketClient::setOnMessageCallback( + OnMessageCallback&& callback) noexcept { + impl_->onMessageCallback_ = std::move(callback); +} + +void WebSocketClient::setOnMessageFrameCallback( + OnMessageFrameCallback&& callback) noexcept { + impl_->onMessageFrameCallback_ = std::move(callback); +} + +void WebSocketClient::setOnClosedFrameCallback( + OnClosedFrameCallback&& callback) noexcept { + impl_->onClosedFrameCallback_ = std::move(callback); +} + +void WebSocketClient::connect( + const std::string& url, + OnConnectCallback&& callback) { + if (thread_) { + react_native_assert(false && "WebSocketClient::connect() called twice"); + return; + } + + impl_->onConnectCallback_ = std::move(callback); + impl_->uri_ = folly::Uri{url}; + // React Native hands its own clients http(s) URLs (Metro's HMR client opens + // `http://host:port/hot`) and every platform's socket takes them as ws(s). + if (impl_->uri_->scheme() == "http") { + impl_->uri_ = folly::Uri{"ws" + url.substr(4)}; + } else if (impl_->uri_->scheme() == "https") { + impl_->uri_ = folly::Uri{"wss" + url.substr(5)}; + } + if (impl_->uri_->scheme() != "ws" && impl_->uri_->scheme() != "wss") { + impl_->onConnectCallback(false, "WebSocket URL must use ws:// or wss://"); + return; + } + if (impl_->uri_->scheme() == "wss") { + try { + impl_->tlsContext_.set_default_verify_paths(); + } catch (const std::exception& error) { + LOG(WARNING) << "[gpui-ws] OpenSSL default trust paths unavailable: " + << error.what(); + } + impl_->tlsContext_.set_verify_mode(boost::asio::ssl::verify_peer); +#if defined(_WIN32) + loadWindowsSystemRoots(impl_->tlsContext_); +#endif + auto ws = impl_->ws_.wlock(); + ws->emplace(impl_->ioContext_, impl_->tlsContext_); + auto& tls = std::get(*ws); + const auto hostname = impl_->uri_->hostname(); + tls.next_layer().set_verify_callback( + [hostname]( + bool preverified, boost::asio::ssl::verify_context& verifyContext) { + if (boost::asio::ssl::host_name_verification(hostname)( + preverified, verifyContext)) { + return true; + } +#if defined(__APPLE__) + return verifyWithMacSystemTrust( + hostname, verifyContext.native_handle()); +#else + return false; +#endif + }); + if (SSL_set_tlsext_host_name( + tls.next_layer().native_handle(), + impl_->uri_->hostname().c_str()) != 1) { + impl_->onConnectCallback(false, "Could not configure TLS server name"); + return; + } + } + + // Resolve the domain name + impl_->resolver_.async_resolve( + impl_->uri_->hostname(), + std::to_string( + impl_->uri_->port() == 0 ? (impl_->uri_->scheme() == "wss" ? 443 : 80) + : impl_->uri_->port()), + [weakImpl = std::weak_ptr(impl_)]( + boost::system::error_code ec, + const boost::asio::ip::tcp::resolver::results_type& results) { + if (auto impl = weakImpl.lock()) { + impl->onResolveCompleted(ec, results); + } + }); + + // Start the I/O thread + static int32_t s_threadId = 0; + thread_ = std::make_unique( + [weakImpl = std::weak_ptr(impl_), tid = s_threadId++]() { + folly::setThreadName("WebSocket" + std::to_string(tid)); + if (auto impl = weakImpl.lock()) { + impl->ioContext_.run(); + } + }); +} + +void WebSocketClient::close(const std::string& reason) { + close(1000, reason); +} + +void WebSocketClient::close(uint16_t code, const std::string& reason) { + if (!impl_->isClosing_.exchange(true)) { + impl_->onClosed(code, reason); + { + std::lock_guard lock(impl_->mutexOut_); + const auto queued = impl_->messagesOut_.size(); + while (!impl_->messagesOut_.empty()) impl_->messagesOut_.pop(); + if (queued > 0) + webSocketMessagesInFlight.fetch_sub(queued, std::memory_order_relaxed); + } + { + auto ws = impl_->ws_.wlock(); + std::visit( + [code, &reason](auto& socket) { + if (socket.is_open()) { + boost::beast::websocket::close_reason closeReason; + closeReason.code = + static_cast(code); + closeReason.reason = reason; + socket.async_close(closeReason, [](auto&&) {}); + } else { + boost::system::error_code ignored; + boost::beast::get_lowest_layer(socket).close(ignored); + } + }, + *ws); + } + // Outside the lock: the I/O thread takes it in every completion handler, + // so joining while holding it deadlocks against a connection that is + // still being set up. + if (thread_) { + thread_->join(); + } + } + thread_ = nullptr; +} + +void WebSocketClient::send(const std::string& message) { + { + std::lock_guard lock(impl_->mutexOut_); + if (impl_->isClosing_) return; + impl_->messagesOut_.emplace(message, false); + webSocketMessagesInFlight.fetch_add(1, std::memory_order_relaxed); + } + impl_->write(); +} + +void WebSocketClient::sendBinary(const std::string& bytes) { + { + std::lock_guard lock(impl_->mutexOut_); + if (impl_->isClosing_) return; + impl_->messagesOut_.emplace(bytes, true); + webSocketMessagesInFlight.fetch_add(1, std::memory_order_relaxed); + } + impl_->write(); +} + +void WebSocketClient::ping() { + auto ws = impl_->ws_.wlock(); + std::visit( + [weakImpl = std::weak_ptr(impl_)](auto& socket) { + socket.async_ping({}, [weakImpl](boost::beast::error_code ec) { + auto impl = weakImpl.lock(); + if (!impl || impl->isClosing_) { + return; + } + if (ec) { + LOG(ERROR) << "Error pinging websocket: " << ec.message(); + } + }); + }, + *ws); +} + +void WebSocketClient::Impl::onResolveCompleted( + boost::system::error_code ec, + const boost::asio::ip::tcp::resolver::results_type& results) { + if (ec) { + // TODO: Handle retry logic here + onConnectCallback(false, "Failed to resolve host"); + return; + } + if (isClosing_) { + // `close()` already closed the socket; connecting now would reopen it and + // keep `close()` waiting on this thread until the attempt fails. + return; + } + + auto ws = ws_.wlock(); + std::visit( + [weakImpl = weak_from_this(), &results](auto& socket) { + boost::asio::async_connect( + boost::beast::get_lowest_layer(socket), + results.begin(), + results.end(), + [weakImpl](boost::system::error_code connectError, const auto&) { + if (auto impl = weakImpl.lock()) { + impl->onConnectionCompleted(connectError); + } + }); + }, + *ws); +} + +void WebSocketClient::Impl::onConnectionCompleted( + boost::system::error_code ec) { + if (ec) { + // TODO: Handle retry logic here + onConnectCallback(false, "Failed to connect"); + return; + } + + { + auto ws = ws_.wlock(); + if (std::holds_alternative(*ws)) { + std::get(*ws).next_layer().async_handshake( + boost::asio::ssl::stream_base::client, + [weakImpl = + weak_from_this()](boost::system::error_code handshakeError) { + if (auto impl = weakImpl.lock()) { + impl->onTlsHandshakeCompleted(handshakeError); + } + }); + return; + } + } + // Outside the lock: `onTlsHandshakeCompleted` takes it again, and it is not + // recursive, so a plain ws:// connection would stop here for good. + onTlsHandshakeCompleted({}); +} + +void WebSocketClient::Impl::onTlsHandshakeCompleted( + boost::system::error_code ec) { + if (ec) { + onConnectCallback(false, "TLS handshake failed: " + ec.message()); + onClosed(1006, ec.message()); + return; + } + auto ws = ws_.wlock(); + // https://datatracker.ietf.org/doc/html/rfc6455#section-3: + // > The "resource-name" (also known as /resource name/ in + // > https://datatracker.ietf.org/doc/html/rfc6455#section-4.1) + // > can be constructed by concatenating the following: + // > + // > o "/" if the path component is empty + // > + // > o the path component + // > + // > o "?" if the query component is non-empty + // > + // > o the query component + auto resourceName = (uri_->path().empty() ? "/" : uri_->path()) + + (uri_->query().empty() ? "" : "?" + uri_->query()); + // Perform the websocket handshake + const auto port = + uri_->port() == 0 ? (uri_->scheme() == "wss" ? 443 : 80) : uri_->port(); + const auto host = uri_->host() + ":" + std::to_string(port); + std::visit( + [weakImpl = weak_from_this(), host, resourceName](auto& socket) { + socket.async_handshake( + host, + resourceName, + [weakImpl](boost::system::error_code handshakeError) { + if (auto impl = weakImpl.lock()) { + impl->onHandshakeCompleted(handshakeError); + } + }); + }, + *ws); +} + +void WebSocketClient::Impl::onHandshakeCompleted(boost::system::error_code ec) { + if (ec) { + // TODO: Handle retry logic here + onConnectCallback(false, "Failed to handshake"); + return; + } + + onConnectCallback(true, "Connected"); + // Listen for any messages from the server + listen(); + + // Start writing any buffered messages + write(); +} + +void WebSocketClient::Impl::listen() { + if (isClosing_) { + return; + } + auto ws = ws_.wlock(); + std::visit( + [this, weakImpl = weak_from_this()](auto& socket) { + socket.async_read( + buffer_, + [weakImpl]( + boost::beast::error_code ec, std::size_t bytes_transferred) { + auto impl = weakImpl.lock(); + if (!impl || impl->isClosing_) { + return; + } + if (ec) { + if (ec == boost::beast::websocket::error::closed) { + auto ws = impl->ws_.rlock(); + const auto& reason = std::visit( + [](const auto& current) -> const auto& { + return current.reason(); + }, + *ws); + impl->onClosed( + static_cast(reason.code), + std::string(reason.reason.data(), reason.reason.size())); + } else { + LOG(ERROR) + << "Error reading from websocket: " << ec.message(); + impl->onClosed(1006, ec.message()); + } + return; + } + std::string message( + boost::beast::buffers_to_string(impl->buffer_.data())); + const bool isBinary = std::visit( + [](const auto& current) { return !current.got_text(); }, + *impl->ws_.rlock()); + if (impl->onMessageCallback_) { + impl->onMessageCallback_(message); + } + if (impl->onMessageFrameCallback_) { + impl->onMessageFrameCallback_(message, isBinary); + } + impl->buffer_.consume(bytes_transferred); + impl->listen(); + }); + }, + *ws); +} + +void WebSocketClient::Impl::write() { + if (isClosing_) { + return; + } + if (isWriting_) { + return; + } + isWriting_ = true; + + std::shared_ptr> message; + { + std::lock_guard lock(mutexOut_); + if (!messagesOut_.empty()) { + message = + std::make_shared>(messagesOut_.front()); + messagesOut_.pop(); + } + } + + if (!message) { + isWriting_ = false; + return; + } + + auto ws = ws_.wlock(); + std::visit( + [message, weakImpl = weak_from_this()](auto& socket) { + socket.binary(message->second); + socket.async_write( + boost::beast::net::buffer(message->first), + [message, weakImpl]( + boost::beast::error_code ec, + std::size_t /*bytes_transferred*/) mutable { + webSocketMessagesInFlight.fetch_sub(1, std::memory_order_relaxed); + auto impl = weakImpl.lock(); + if (!impl) { + return; + } + if (ec) { + LOG(ERROR) << "Error writing to websocket: " << ec.message(); + impl->isWriting_ = false; + return; + } + impl->isWriting_ = false; + if (!impl->isClosing_) impl->write(); + message.reset(); // Release the message after it's been sent + }); + }, + *ws); +} + +void WebSocketClient::Impl::onConnectCallback( + bool connected, + const std::string& error) { + if (!connected) { + LOG(WARNING) << "WebSocket " << (uri_ ? uri_->host() : "") << " did not connect: " << error; + } + if (onConnectCallback_) { + onConnectCallback_(connected, error); + } +} + +void WebSocketClient::Impl::onClosed(uint16_t code, const std::string& reason) { + if (didReportClosed_.exchange(true)) { + return; + } + if (onClosedFrameCallback_) { + onClosedFrameCallback_(code, reason); + } + if (onClosedCallback_) { + onClosedCallback_(reason); + } +} + +WebSocketClientFactory getWebSocketClientFactory() { + return []() { return std::make_unique(); }; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/platform/cxx/WebSocketClient.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/platform/cxx/WebSocketClient.h new file mode 100644 index 0000000000..9526c0cf24 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/http/platform/cxx/WebSocketClient.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class WebSocketClient : public IWebSocketClient { + public: + WebSocketClient() noexcept; + ~WebSocketClient() override; + WebSocketClient(WebSocketClient &other) = delete; + WebSocketClient &operator=(WebSocketClient &other) = delete; + WebSocketClient(WebSocketClient &&other) = delete; + WebSocketClient &operator=(WebSocketClient &&other) = delete; + + void setOnClosedCallback(OnClosedCallback &&callback) noexcept override; + + void setOnMessageCallback(OnMessageCallback &&callback) noexcept override; + void setOnMessageFrameCallback(OnMessageFrameCallback &&callback) noexcept override; + void setOnClosedFrameCallback(OnClosedFrameCallback &&callback) noexcept override; + + void connect(const std::string &url, OnConnectCallback &&onConnectCallback = nullptr) override; + + void close(const std::string &reason) override; + void close(uint16_t code, const std::string &reason) override; + + void send(const std::string &message) override; + void sendBinary(const std::string &bytes) override; + + void ping() override; + + private: + struct Impl; + + // Instance data and IO thread + const std::shared_ptr impl_; + std::unique_ptr thread_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/BlobModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/BlobModule.cpp new file mode 100644 index 0000000000..a4317224c4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/BlobModule.cpp @@ -0,0 +1,133 @@ +#include "BlobModule.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { +namespace { +std::mutex blobMutex; +std::unordered_map blobs; +std::unordered_set blobWebSocketHandlers; + +http::BlobReference blobReference(jsi::Runtime& rt, const jsi::Object& value) { + http::BlobReference reference; + reference.blobId = value.getProperty(rt, "blobId").asString(rt).utf8(rt); + reference.offset = static_cast( + value.getProperty(rt, "offset").asNumber()); + reference.size = static_cast( + value.getProperty(rt, "size").asNumber()); + return reference; +} +} // namespace + +BlobModule::BlobModule(std::shared_ptr jsInvoker) + : NativeBlobModuleCxxSpec(std::move(jsInvoker)) {} + +jsi::Object BlobModule::getConstants(jsi::Runtime& rt) { + jsi::Object constants(rt); + constants.setProperty( + rt, "BLOB_URI_SCHEME", jsi::String::createFromAscii(rt, "blob")); + constants.setProperty( + rt, "BLOB_URI_HOST", jsi::String::createFromAscii(rt, "gpui.local")); + return constants; +} + +void BlobModule::addNetworkingHandler(jsi::Runtime& /*rt*/) {} + +void BlobModule::addWebSocketHandler(jsi::Runtime& /*rt*/, double socketId) { + setBlobWebSocketHandler(static_cast(socketId), true); +} + +void BlobModule::removeWebSocketHandler( + jsi::Runtime& /*rt*/, + double socketId) { + setBlobWebSocketHandler(static_cast(socketId), false); +} + +void BlobModule::sendOverSocket( + jsi::Runtime& rt, + jsi::Object blob, + double socketId) { + auto reference = blobReference(rt, blob); + auto bytes = readBlob(reference.blobId, reference.offset, reference.size); + if (!bytes) { + throw jsi::JSError(rt, "Blob data is no longer available"); + } + if (!sendBlobOverWebSocket(static_cast(socketId), *bytes)) { + throw jsi::JSError(rt, "WebSocket is no longer available"); + } +} + +void BlobModule::createFromParts( + jsi::Runtime& rt, + jsi::Array parts, + std::string blobId) { + std::string bytes; + for (size_t index = 0; index < parts.size(rt); ++index) { + auto part = parts.getValueAtIndex(rt, index).asObject(rt); + auto type = part.getProperty(rt, "type").asString(rt).utf8(rt); + auto data = part.getProperty(rt, "data"); + if (type == "string") { + bytes += data.asString(rt).utf8(rt); + } else if (type == "blob") { + auto reference = blobReference(rt, data.asObject(rt)); + auto content = readBlob(reference.blobId, reference.offset, reference.size); + if (!content) { + throw jsi::JSError(rt, "Blob part data is no longer available"); + } + bytes += *content; + } else { + throw jsi::JSError(rt, "Unsupported Blob part type"); + } + } + storeBlob(std::move(blobId), std::move(bytes)); +} + +void BlobModule::release(jsi::Runtime& /*rt*/, std::string blobId) { + eraseBlob(blobId); +} + +void storeBlob(std::string blobId, std::string bytes) { + std::lock_guard lock(blobMutex); + blobs[std::move(blobId)] = std::move(bytes); +} + +std::optional readBlob( + const std::string& blobId, + uint64_t offset, + uint64_t size) { + std::lock_guard lock(blobMutex); + auto it = blobs.find(blobId); + if (it == blobs.end() || offset > it->second.size()) { + return std::nullopt; + } + const auto available = static_cast(it->second.size()) - offset; + const auto length = std::min(size, available); + return it->second.substr(static_cast(offset), static_cast(length)); +} + +void eraseBlob(const std::string& blobId) { + std::lock_guard lock(blobMutex); + blobs.erase(blobId); +} + +void setBlobWebSocketHandler(int32_t socketId, bool enabled) { + std::lock_guard lock(blobMutex); + if (enabled) { + blobWebSocketHandlers.insert(socketId); + } else { + blobWebSocketHandlers.erase(socketId); + } +} + +bool isBlobWebSocketHandler(int32_t socketId) { + std::lock_guard lock(blobMutex); + return blobWebSocketHandlers.contains(socketId); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/BlobModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/BlobModule.h new file mode 100644 index 0000000000..9e2731d7f6 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/BlobModule.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class BlobModule final : public NativeBlobModuleCxxSpec { + public: + explicit BlobModule(std::shared_ptr jsInvoker); + + jsi::Object getConstants(jsi::Runtime& rt); + void addNetworkingHandler(jsi::Runtime& rt); + void addWebSocketHandler(jsi::Runtime& rt, double socketId); + void removeWebSocketHandler(jsi::Runtime& rt, double socketId); + void sendOverSocket(jsi::Runtime& rt, jsi::Object blob, double socketId); + void createFromParts(jsi::Runtime& rt, jsi::Array parts, std::string blobId); + void release(jsi::Runtime& rt, std::string blobId); +}; + +void storeBlob(std::string blobId, std::string bytes); +std::optional readBlob( + const std::string& blobId, + uint64_t offset, + uint64_t size); +void eraseBlob(const std::string& blobId); +void setBlobWebSocketHandler(int32_t socketId, bool enabled); +bool isBlobWebSocketHandler(int32_t socketId); +bool sendBlobOverWebSocket(int32_t socketId, const std::string& bytes); + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/CMakeLists.txt new file mode 100644 index 0000000000..42c589f05c --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_io_SRC CONFIGURE_DEPENDS *.cpp platform/cxx/*.cpp) +add_library(react_cxx_platform_react_io OBJECT ${react_cxx_platform_react_io_SRC}) + +target_include_directories(react_cxx_platform_react_io PUBLIC ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_io + glog + jsi + react_cxx_platform_react_nativemodule + react_cxx_platform_react_http + react_cxx_platform_react_threading + react_codegen_rncore + react_bridging +) +target_compile_reactnative_options(react_cxx_platform_react_io PRIVATE) +target_compile_options(react_cxx_platform_react_io PRIVATE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/FileReaderModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/FileReaderModule.cpp new file mode 100644 index 0000000000..2e83f85072 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/FileReaderModule.cpp @@ -0,0 +1,58 @@ +#include "FileReaderModule.h" + +#include +#include +#include + +namespace facebook::react { +namespace { +http::BlobReference readBlobReference( + jsi::Runtime& rt, + const jsi::Object& blob) { + return http::BlobReference{ + .blobId = blob.getProperty(rt, "blobId").asString(rt).utf8(rt), + .offset = static_cast( + blob.getProperty(rt, "offset").asNumber()), + .size = static_cast(blob.getProperty(rt, "size").asNumber())}; +} +} // namespace + +FileReaderModule::FileReaderModule(std::shared_ptr jsInvoker) + : NativeFileReaderModuleCxxSpec(jsInvoker), + jsInvoker_(std::move(jsInvoker)) {} + +AsyncPromise FileReaderModule::readAsDataURL( + jsi::Runtime& rt, + jsi::Object blob) { + AsyncPromise promise(rt, jsInvoker_); + const auto reference = readBlobReference(rt, blob); + const auto bytes = readBlob(reference.blobId, reference.offset, reference.size); + if (!bytes) { + promise.reject("Blob data is no longer available"); + return promise; + } + const auto type = blob.getProperty(rt, "type").asString(rt).utf8(rt); + promise.resolve("data:" + type + ";base64," + base64Encode(*bytes)); + return promise; +} + +AsyncPromise FileReaderModule::readAsText( + jsi::Runtime& rt, + jsi::Object blob, + std::string encoding) { + AsyncPromise promise(rt, jsInvoker_); + if (encoding != "utf-8" && encoding != "UTF-8" && encoding != "utf8") { + promise.reject("Only UTF-8 Blob text decoding is currently supported"); + return promise; + } + const auto reference = readBlobReference(rt, blob); + const auto bytes = readBlob(reference.blobId, reference.offset, reference.size); + if (!bytes) { + promise.reject("Blob data is no longer available"); + } else { + promise.resolve(*bytes); + } + return promise; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/FileReaderModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/FileReaderModule.h new file mode 100644 index 0000000000..ae180d22d1 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/FileReaderModule.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class FileReaderModule final + : public NativeFileReaderModuleCxxSpec { + public: + explicit FileReaderModule(std::shared_ptr jsInvoker); + + AsyncPromise readAsDataURL( + jsi::Runtime& rt, + jsi::Object blob); + AsyncPromise readAsText( + jsi::Runtime& rt, + jsi::Object blob, + std::string encoding); + + private: + std::shared_ptr jsInvoker_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/IImageLoader.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/IImageLoader.h new file mode 100644 index 0000000000..8362646682 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/IImageLoader.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +using IImageLoaderOnLoadCallback = std::function; + +class IImageLoader { + public: + enum class CacheStatus { + None = 0, + Disk = 0x1, + Memory = 0x1 << 1, + }; + + virtual ~IImageLoader() = default; + + virtual void loadImage(const std::string &uri, const IImageLoaderOnLoadCallback &&onLoad) = 0; + + virtual CacheStatus getCacheStatus(const std::string &uri) = 0; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ImageLoaderModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ImageLoaderModule.cpp new file mode 100644 index 0000000000..7b52271ee8 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ImageLoaderModule.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ImageLoaderModule.h" +#include "IImageLoader.h" + +namespace facebook::react { + +jsi::Object ImageLoaderModule::getConstants(jsi::Runtime& rt) { + return jsi::Object(rt); +} + +void ImageLoaderModule::abortRequest(jsi::Runtime& rt, int32_t requestId) {} + +AsyncPromise> ImageLoaderModule::getSize( + jsi::Runtime& rt, + const std::string& uri) { + auto promise = AsyncPromise>(rt, jsInvoker_); + if (auto imageLoader = imageLoader_.lock()) { + imageLoader->loadImage( + uri, + [promise]( + double imageWidth, + double imageHeight, + const char* errorMessage) mutable { + if (errorMessage == nullptr) { + promise.resolve({imageWidth, imageHeight}); + } else { + promise.reject(errorMessage); + } + }); + } else { + promise.reject("Failed to get image size: image loader is not available."); + } + return promise; +} + +AsyncPromise ImageLoaderModule::getSizeWithHeaders( + jsi::Runtime& rt, + const std::string& uri, + jsi::Object /*headers*/) { + auto promise = AsyncPromise(rt, jsInvoker_); + if (auto imageLoader = imageLoader_.lock()) { + imageLoader->loadImage(uri, [promise](double width, double height, const char* error) mutable { + if (error == nullptr) promise.resolve({.width = width, .height = height}); + else promise.reject(error); + }); + } else { + promise.reject("Failed to get image size: image loader is not available."); + } + return promise; +} + +AsyncPromise ImageLoaderModule::prefetchImage( + jsi::Runtime& rt, + const std::string& uri) { + auto promise = AsyncPromise(rt, jsInvoker_); + if (auto imageLoader = imageLoader_.lock()) { + imageLoader->loadImage( + uri, + [promise]( + double /*imageWidth*/, + double /*imageHeight*/, + const char* errorMessage) mutable { + if (errorMessage == nullptr) { + promise.resolve(true); + } else { + promise.reject(errorMessage); + } + }); + } else { + promise.reject("Failed to get image size: image loader is not available."); + } + return promise; +} + +AsyncPromise ImageLoaderModule::prefetchImageWithMetadata( + jsi::Runtime& rt, + const std::string& uri, + const std::string& /*queryRootName*/, + int32_t /*rootTag*/) { + return prefetchImage(rt, uri); +} + +jsi::Object ImageLoaderModule::queryCache( + jsi::Runtime& rt, + const std::vector& uris) { + auto result = jsi::Object(rt); + if (auto imageLoader = imageLoader_.lock()) { + for (const auto& uri : uris) { + auto cacheStatus = static_cast(imageLoader->getCacheStatus(uri)); + if (cacheStatus == static_cast(IImageLoader::CacheStatus::None)) { + continue; + } + const bool isOnDisk = + (cacheStatus & static_cast(IImageLoader::CacheStatus::Disk)) != + 0; + const bool isInMemory = + (cacheStatus & static_cast(IImageLoader::CacheStatus::Memory)) != + 0; + + std::string cacheStatusString; + if (isOnDisk && isInMemory) { + cacheStatusString = "disk/memory"; + } else if (isInMemory) { + cacheStatusString = "memory"; + } else if (isOnDisk) { + cacheStatusString = "disk"; + } + result.setProperty(rt, uri.c_str(), cacheStatusString.c_str()); + } + } + return result; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ImageLoaderModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ImageLoaderModule.h new file mode 100644 index 0000000000..2980d9df46 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ImageLoaderModule.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class IImageLoader; + +using ImageSize = NativeImageLoaderAndroidImageSize; + +template <> +struct Bridging : NativeImageLoaderAndroidImageSizeBridging {}; + +class ImageLoaderModule : public NativeImageLoaderIOSCxxSpec { + public: + explicit ImageLoaderModule( + std::shared_ptr jsInvoker, + std::weak_ptr imageLoader = std::weak_ptr()) + : NativeImageLoaderIOSCxxSpec(jsInvoker), imageLoader_(std::move(imageLoader)) + { + } + + jsi::Object getConstants(jsi::Runtime &rt); + void abortRequest(jsi::Runtime &rt, int32_t requestId); + + AsyncPromise> getSize(jsi::Runtime &rt, const std::string &uri); + + AsyncPromise getSizeWithHeaders(jsi::Runtime &rt, const std::string &uri, jsi::Object headers); + + AsyncPromise prefetchImage(jsi::Runtime &rt, const std::string &uri); + + AsyncPromise prefetchImageWithMetadata( + jsi::Runtime &rt, + const std::string &uri, + const std::string &queryRootName, + int32_t rootTag); + + jsi::Object queryCache(jsi::Runtime &rt, const std::vector &uris); + + private: + std::weak_ptr imageLoader_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/NetworkingModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/NetworkingModule.cpp new file mode 100644 index 0000000000..e06f015026 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/NetworkingModule.cpp @@ -0,0 +1,404 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "NetworkingModule.h" +#include + +#include +#include + +namespace facebook::react { + +// The requests abstraction is here to deal with a family of race conditions +// which can happen due to the async nature of this code: +// +// * store token vs callback +// * callback vs cancel +// * stop vs everything +// +// All of the access to the Requests data is protected by a mutex, so it is +// effectively serialized. The code takes care to handle execution in any +// order, so long as reserve for an id precedes any other operation on that id; +// the tests verify that behavior is correct for all use cases. + +Requests::~Requests() { + react_native_assert(isStopped() && "Requests::stop() was not called"); +} + +bool Requests::reserve(uint32_t id) { + if (stopped_) { + return false; + } + std::lock_guard lock(tokensMutex_); + tokens_.emplace(id, nullptr); + return true; +} + +void Requests::store(uint32_t id, std::unique_ptr token) { + // Three possibilities here: + + // 1. HttpClient is shutting down. Cancel the token, don't save it. + // 2. The entry in tokens_ has been erased, which means the callbacks have + // run. We cancel, which should be harmless, just in case. Nothing to save. + // 3. HttpClient and the request are still running. Save the token. + + { + if (stopped_) { + // option 1: fall past the lock and cancel + } else { + std::lock_guard lock(tokensMutex_); + if (auto iter = tokens_.find(id); iter == tokens_.end()) { + // option 2: fall past the lock and cancel + } else { + // option 3: save the token + iter->second = std::move(token); + return; + } + } + } + + if (token) { + token->cancel(); + } +} + +bool Requests::erase(uint32_t id) { + if (stopped_) { + return false; + } + std::lock_guard lock(tokensMutex_); + tokens_.erase(id); + return true; +} + +void Requests::cancel(uint32_t id) { + std::lock_guard lock(tokensMutex_); + if (auto iter = tokens_.find(id); iter != tokens_.end()) { + if (iter->second) { + iter->second->cancel(); + } + tokens_.erase(iter); + } +} + +void Requests::stop() { + stopped_ = true; + decltype(tokens_) tokensCopy; + { + std::lock_guard lock(tokensMutex_); + tokensCopy.swap(tokens_); + } + + for (const auto& token : tokensCopy) { + if (token.second) { + token.second->cancel(); + } + } +} + +bool Requests::isStopped() { + if (stopped_) { + std::lock_guard lock(tokensMutex_); + assert(tokens_.empty()); + } + return stopped_; +} + +NetworkingModule::NetworkingModule( + std::shared_ptr jsInvoker, + const HttpClientFactory& httpClientFactory) + : NativeNetworkingAndroidCxxSpec(jsInvoker) { + httpClient_ = httpClientFactory(); +} + +NetworkingModule::~NetworkingModule() { + requests_.stop(); +} + +void NetworkingModule::sendRequest( + jsi::Runtime& /*rt*/, + const std::string& method, + const std::string& url, + uint32_t requestId, + const http::Headers& headers, + const http::Body& body, + const std::string& responseType, + bool useIncrementalUpdates, + uint32_t timeout, + bool withCredentials) { + if (!requests_.reserve(requestId)) { + // If the NetworkingModule is shutting down, don't invoke the callback. + return; + } + + bool sendIncrementalUpdates = responseType == "text" && useIncrementalUpdates; + bool sendProgressUpdates = responseType != "text" && useIncrementalUpdates; + + http::NetworkCallbacks callbacks{ + .onUploadProgress = + [weakThis = weak_from_this(), requestId]( + int64_t progress, int64_t total) { + if (auto strongThis = weakThis.lock()) { + strongThis->didSendNetworkData(requestId, progress, total); + } + }, + .onResponse = + [weakThis = weak_from_this(), requestId]( + uint32_t responseCode, + const http::Headers& headers, + const std::string& responseUrl) { + if (auto strongThis = weakThis.lock()) { + strongThis->didReceiveNetworkResponse( + requestId, responseCode, headers, responseUrl); + } + }, + .onBody = + [weakThis = weak_from_this(), requestId, responseType]( + std::unique_ptr data) { + if (auto strongThis = weakThis.lock()) { + try { + strongThis->didReceiveNetworkData( + requestId, responseType, std::move(data)); + } catch (const std::exception& e) { + LOG(ERROR) << "Failed to parse network response body: " + << e.what(); + } catch (...) { + LOG(ERROR) << "Failed to parse network response body."; + } + } + }, + .onBodyIncremental = + [weakThis = weak_from_this(), requestId]( + int64_t progress, + int64_t total, + std::unique_ptr body) { + int64_t bytesRead = 0; + if (auto strongThis = weakThis.lock()) { + bytesRead = strongThis->didReceiveNetworkIncrementalData( + requestId, std::move(body), progress, total); + } + return bytesRead; + }, + .onBodyProgress = + [weakThis = weak_from_this(), requestId]( + int64_t loaded, int64_t total) { + if (auto strongThis = weakThis.lock()) { + strongThis->didReceiveNetworkDataProgress( + requestId, loaded, total); + } + }, + .onResponseComplete = + [weakThis = weak_from_this(), requestId]( + const std::string& error, bool timeout) { + if (auto strongThis = weakThis.lock()) { + strongThis->didCompleteNetworkResponse(requestId, error, timeout); + } + }, + .sendIncrementalUpdates = sendIncrementalUpdates, + .sendProgressUpdates = sendProgressUpdates}; + + auto requestBody = body; + requestBody.withCredentials = withCredentials; + auto token = httpClient_->sendRequest( + std::move(callbacks), + method, + url, + headers, + requestBody, + timeout, + std::to_string(requestId)); + if (token) { + requests_.store(requestId, std::move(token)); + } +} + +void NetworkingModule::didSendNetworkData( + uint32_t requestId, + int64_t progress, + int64_t total) { + if (requests_.isStopped()) { + return; + } + emitDeviceEvent( + "didSendNetworkData", + [requestId, progress, total, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + auto result = std::make_tuple( + requestId, + static_cast(progress), + static_cast(total)); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + }); +} + +void NetworkingModule::didReceiveNetworkResponse( + uint32_t requestId, + uint32_t statusCode, + const http::Headers& headers, + const std::string& responseUrl) noexcept { + if (requests_.isStopped()) { + return; + } + emitDeviceEvent( + "didReceiveNetworkResponse", + [requestId, statusCode, headers, responseUrl, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + auto result = + std::make_tuple(requestId, statusCode, headers, responseUrl); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + }); +} + +int64_t NetworkingModule::didReceiveNetworkIncrementalData( + uint32_t requestId, + std::unique_ptr buf, + int64_t progress, + int64_t total) { + auto data = buf->moveToFbString().toStdString(); + auto bytesRead = data.size(); + emitDeviceEvent( + "didReceiveNetworkIncrementalData", + [requestId, data, progress, total, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, + static_cast(requestId), + data, + static_cast(progress), + static_cast(total))); + }); + return bytesRead; +}; + +namespace { +// Encodes a response body for delivery to JS according to responseType. +// +// The JS XMLHttpRequest layer base64-decodes the delivered string for +// 'base64'-type responses (responseType 'arraybuffer'), so binary bodies must +// be base64-encoded here, matching the Android and iOS NetworkingModule +// implementations. Without this, base64.toByteArray() on the JS side +// mis-decodes the raw payload and corrupts the response (e.g. JSON.parse +// failures on arraybuffer fetches). All other response types are delivered +// unchanged. +std::string encodeResponseBody( + const std::string& responseType, + std::string body) { + if (responseType == "base64") { + return base64Encode(body); + } + return body; +} +} // namespace + +void NetworkingModule::didReceiveNetworkData( + uint32_t requestId, + const std::string& responseType, + std::unique_ptr buf) { + if (requests_.isStopped()) { + return; + } + + auto body = buf->toString(); + if (responseType == "blob") { + static std::atomic blobSequence{0}; + const auto blobId = "gpui-response-" + std::to_string(requestId) + "-" + + std::to_string(blobSequence.fetch_add(1)); + const auto size = body.size(); + storeBlob(blobId, std::move(body)); + emitDeviceEvent( + "didReceiveNetworkData", + [requestId, blobId, size, jsInvoker = jsInvoker_] ( + jsi::Runtime& rt, std::vector& args) { + jsi::Object data(rt); + data.setProperty( + rt, "blobId", jsi::String::createFromUtf8(rt, blobId)); + data.setProperty(rt, "offset", 0); + data.setProperty(rt, "size", static_cast(size)); + data.setProperty(rt, "type", ""); + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, static_cast(requestId), std::move(data))); + }); + return; + } + auto responseData = encodeResponseBody(responseType, std::move(body)); + emitDeviceEvent( + "didReceiveNetworkData", + [requestId, + responseData = std::move(responseData), + jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, static_cast(requestId), responseData)); + }); +} + +void NetworkingModule::didReceiveNetworkDataProgress( + uint32_t requestId, + int64_t bytesRead, + int64_t total) { + emitDeviceEvent( + "didReceiveNetworkDataProgress", + [requestId, bytesRead, total, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back( + rt, + jsi::Array::createWithElements( + rt, + static_cast(requestId), + static_cast(bytesRead), + static_cast(total))); + }); +} + +void NetworkingModule::didCompleteNetworkResponse( + uint32_t requestId, + const std::string& error, + bool timeoutError) { + if (requests_.isStopped() || !requests_.erase(requestId)) { + // If the HttpClient is shutting down, don't invoke the callback. + return; + } + emitDeviceEvent( + "didCompleteNetworkResponse", + [requestId, error, timeoutError, jsInvoker = jsInvoker_]( + jsi::Runtime& rt, std::vector& args) { + if (error.empty()) { + auto result = std::make_tuple(static_cast(requestId)); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + } else { + auto result = std::make_tuple( + static_cast(requestId), error, timeoutError); + args.emplace_back(bridging::toJs(rt, result, jsInvoker)); + } + }); +} + +void NetworkingModule::abortRequest(jsi::Runtime& /*rt*/, uint32_t requestId) { + requests_.cancel(requestId); +} + +void NetworkingModule::clearCookies( + jsi::Runtime& /*rt*/, + const AsyncCallback& callback) { + callback(rngpui::gpui_http_client_clear_cookies()); +} + +void NetworkingModule::addListener( + jsi::Runtime& /*rt*/, + const std::string& /*eventName*/) {} + +void NetworkingModule::removeListeners( + jsi::Runtime& /*rt*/, + uint32_t /*count*/) {} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/NetworkingModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/NetworkingModule.h new file mode 100644 index 0000000000..54c4bc28fc --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/NetworkingModule.h @@ -0,0 +1,127 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +template <> +struct Bridging { + static http::FormDataField + fromJs(jsi::Runtime &rt, const jsi::Object &value, const std::shared_ptr &jsInvoker) + { + auto fieldName = bridging::fromJs(rt, value.getProperty(rt, "fieldName"), jsInvoker); + auto headers = bridging::fromJs(rt, value.getProperty(rt, "headers"), jsInvoker); + auto string = bridging::fromJs>(rt, value.getProperty(rt, "string"), jsInvoker); + auto uri = bridging::fromJs>(rt, value.getProperty(rt, "uri"), jsInvoker); + return http::FormDataField{.fieldName = fieldName, .headers = headers, .string = string, .uri = uri}; + } +}; + +template <> +struct Bridging { + static http::Body fromJs(jsi::Runtime &rt, const jsi::Object &value, const std::shared_ptr &jsInvoker) + { + return http::Body{ + .string = bridging::fromJs>(rt, value.getProperty(rt, "string"), jsInvoker), + .blob = value.getProperty(rt, "blob").isObject() + ? std::optional(http::BlobReference{ + .blobId = value.getProperty(rt, "blob").asObject(rt).getProperty(rt, "blobId").asString(rt).utf8(rt), + .offset = static_cast(value.getProperty(rt, "blob").asObject(rt).getProperty(rt, "offset").asNumber()), + .size = static_cast(value.getProperty(rt, "blob").asObject(rt).getProperty(rt, "size").asNumber())}) + : std::nullopt, + .formData = bridging::fromJs>(rt, value.getProperty(rt, "formData"), jsInvoker), + .base64 = bridging::fromJs>(rt, value.getProperty(rt, "base64"), jsInvoker), + .withCredentials = true, + + }; + } +}; + +class Requests { + public: + Requests() noexcept = default; + ~Requests(); + Requests(Requests &other) = delete; + Requests &operator=(Requests &other) = delete; + Requests(Requests &&other) = delete; + Requests &operator=(Requests &&other) = delete; + + bool reserve(uint32_t id); + void store(uint32_t id, std::unique_ptr token); + bool erase(uint32_t id); + void cancel(uint32_t id); + void stop(); + bool isStopped(); + + private: + std::mutex tokensMutex_; + std::unordered_map> tokens_; + std::atomic stopped_{false}; +}; + +// Implementation of "Networking" TurboModule +class NetworkingModule : public NativeNetworkingAndroidCxxSpec, + public std::enable_shared_from_this { + public: + NetworkingModule(std::shared_ptr jsInvoker, const HttpClientFactory &httpClientFactory); + + ~NetworkingModule() override; + + void sendRequest( + jsi::Runtime &rt, + const std::string &method, + const std::string &url, + uint32_t requestId, + const http::Headers &headers, + const http::Body &body, + const std::string &responseType, + bool useIncrementalUpdates, + uint32_t timeout, + bool withCredentials); + + void abortRequest(jsi::Runtime &rt, uint32_t requestId); + + void clearCookies(jsi::Runtime &rt, const AsyncCallback &callback); + + // RCTEventEmitter + void addListener(jsi::Runtime &rt, const std::string &eventName); + void removeListeners(jsi::Runtime &rt, uint32_t count); + + private: + void didSendNetworkData(uint32_t requestId, int64_t progress, int64_t total); + + void didReceiveNetworkResponse( + uint32_t requestId, + uint32_t statusCode, + const http::Headers &headers, + const std::string &responseUrl) noexcept; + + void didReceiveNetworkData(uint32_t requestId, const std::string &responseType, std::unique_ptr buf); + + int64_t didReceiveNetworkIncrementalData( + uint32_t requestId, + std::unique_ptr buf, + int64_t progress, + int64_t total); + + void didReceiveNetworkDataProgress(uint32_t requestId, int64_t bytesRead, int64_t total); + + void didCompleteNetworkResponse(uint32_t requestId, const std::string &error, bool timeoutError); + + std::unique_ptr httpClient_; + Requests requests_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ResourceLoader.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ResourceLoader.cpp new file mode 100644 index 0000000000..dd7e681719 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ResourceLoader.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResourceLoader.h" + +#include +#include + +namespace facebook::react { +/* static */ bool ResourceLoader::isAbsolutePath(const std::string& path) { + return std::filesystem::path(path).is_absolute(); +} + +/* static */ bool ResourceLoader::isDirectory(const std::string& path) { + if (isAbsolutePath(path) && std::filesystem::is_directory(path)) { + return true; + } + + return isResourceDirectory(path); +} + +/* static */ bool ResourceLoader::isFile(const std::string& path) { + if (isAbsolutePath(path) && std::filesystem::exists(path) && + !std::filesystem::is_directory(path)) { + return true; + } + + return isResourceFile(path); +} + +/* static */ std::unique_ptr ResourceLoader::getFileContents( + const std::string& path) { + if (isResourceFile(path)) { + return getResourceFileContents(path); + } else { + return JSBigFileString::fromPath(path); + } +} + +/* static */ std::filesystem::path ResourceLoader::getCacheDirectory( + const std::string& path) { + auto root = getCacheRootPath() / CACHE_DIR; + if (!std::filesystem::exists(root)) { + try { + std::filesystem::create_directory(root); + } catch (...) { + LOG(ERROR) << "Failed to create root cache directory: " << root; + throw; + } + } + + if (path.empty()) { + return root; + } + + auto dir = root / path; + try { + std::filesystem::create_directories(dir); + return dir; + } catch (...) { + LOG(ERROR) << "Failed to create cache directory: " << dir; + throw; + } +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ResourceLoader.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ResourceLoader.h new file mode 100644 index 0000000000..8b4588a1e8 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/ResourceLoader.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class JSBigString; + +class ResourceLoader { + public: + static bool isDirectory(const std::string &path); + static bool isFile(const std::string &path); + static bool isAbsolutePath(const std::string &path); + static std::unique_ptr getFileContents(const std::string &path); + static std::filesystem::path getCacheDirectory(const std::string &path = std::string()); + + protected: + static bool isResourceDirectory(const std::string &path); + static bool isResourceFile(const std::string &path); + static std::unique_ptr getResourceFileContents(const std::string &path); + + private: + static constexpr const auto CACHE_DIR = ".react-native-cxx-cache"; + static std::filesystem::path getCacheRootPath(); +}; +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/WebSocketModule.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/WebSocketModule.cpp new file mode 100644 index 0000000000..3ed94c2d1c --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/WebSocketModule.cpp @@ -0,0 +1,252 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "WebSocketModule.h" + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { +std::mutex socketSendersMutex; +std::unordered_map> socketSenders; + +void removeSocketSender(int32_t socketId) { + std::lock_guard lock(socketSendersMutex); + socketSenders.erase(socketId); +} +} // namespace + +bool sendBlobOverWebSocket(int32_t socketId, const std::string& bytes) { + std::function sender; + { + std::lock_guard lock(socketSendersMutex); + auto it = socketSenders.find(socketId); + if (it == socketSenders.end()) return false; + sender = it->second; + } + sender(bytes); + return true; +} + +WebSocketModule::WebSocketModule( + std::shared_ptr jsInvoker, + WebSocketClientFactory webSocketClientFactory) + : NativeWebSocketModuleCxxSpec(jsInvoker), + webSocketClientFactory_(std::move(webSocketClientFactory)) {} + +WebSocketModule::~WebSocketModule() { + for (const auto& [socketId, _] : connections_) { + removeSocketSender(socketId); + } + connections_.clear(); +} + +void WebSocketModule::connect( + jsi::Runtime& /*rt*/, + const std::string& url, + const std::optional>& /*protocols*/, + jsi::Object /*options*/, + int32_t socketID) { + auto webSocket = webSocketClientFactory_(); + connections_.emplace(socketID, std::move(webSocket)); + { + std::lock_guard lock(socketSendersMutex); + socketSenders[socketID] = [weakThis = weak_from_this(), socketID]( + const std::string& bytes) { + if (auto strongThis = weakThis.lock()) { + auto it = strongThis->connections_.find(socketID); + if (it != strongThis->connections_.end()) { + it->second->sendBinary(bytes); + } + } + }; + } + connections_[socketID]->setOnMessageFrameCallback([weakThis = weak_from_this(), + socketID]( + const std::string& message, + bool isBinary) { + auto strongThis = weakThis.lock(); + if (!strongThis) { + return; + } + const bool asBlob = isBinary && isBlobWebSocketHandler(socketID); + static std::atomic blobSequence{0}; + const auto blobId = asBlob + ? "gpui-websocket-" + std::to_string(socketID) + "-" + + std::to_string(blobSequence.fetch_add(1)) + : std::string{}; + if (asBlob) { + storeBlob(blobId, message); + } + strongThis->emitDeviceEvent( + "websocketMessage", + [socketID, message = std::string(message), isBinary, asBlob, blobId]( + jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + arg.setProperty( + rt, + "type", + facebook::jsi::String::createFromAscii( + rt, asBlob ? "blob" : (isBinary ? "binary" : "string"))); + if (asBlob) { + jsi::Object data(rt); + data.setProperty( + rt, "blobId", jsi::String::createFromUtf8(rt, blobId)); + data.setProperty(rt, "offset", 0); + data.setProperty(rt, "size", static_cast(message.size())); + data.setProperty(rt, "type", "application/octet-stream"); + arg.setProperty(rt, "data", std::move(data)); + } else { + arg.setProperty( + rt, + "data", + facebook::jsi::String::createFromUtf8( + rt, isBinary ? base64Encode(message) : message)); + } + args.emplace_back(rt, arg); + }); + }); + connections_[socketID]->setOnClosedFrameCallback( + [weakThis = weak_from_this(), socketID](uint16_t code, const std::string& reason) { + removeSocketSender(socketID); + setBlobWebSocketHandler(socketID, false); + auto strongThis = weakThis.lock(); + if (!strongThis) { + return; + } + strongThis->emitDeviceEvent( + "websocketClosed", + [socketID, code, reason](jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + arg.setProperty(rt, "code", jsi::Value(code)); + arg.setProperty( + rt, + "reason", + facebook::jsi::String::createFromUtf8(rt, reason)); + args.emplace_back(rt, arg); + }); + }); + + connections_[socketID]->connect( + url, + [weakThis = weak_from_this(), socketID]( + bool success, const std::optional& /*message*/) { + auto strongThis = weakThis.lock(); + if (!strongThis) { + return; + } + if (success) { + strongThis->emitDeviceEvent( + "websocketOpen", + [socketID](jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + args.emplace_back(rt, arg); + }); + } else { + strongThis->emitDeviceEvent( + "websocketFailed", + [socketID](jsi::Runtime& rt, std::vector& args) { + auto arg = jsi::Object(rt); + arg.setProperty(rt, "id", jsi::Value(socketID)); + arg.setProperty( + rt, + "message", + facebook::jsi::String::createFromAscii( + rt, "Could not connect to websocket.")); + args.emplace_back(rt, arg); + }); + } + }); +} + +void WebSocketModule::send( + jsi::Runtime& /*rt*/, + const std::string& message, + int32_t socketID) { + auto it = connections_.find(static_cast(socketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to send data to a socket (" << socketID + << "), the socket is not open."; + return; + } + it->second->send(message); +} + +void WebSocketModule::sendBinary( + jsi::Runtime& rt, + const std::string& base64String, + int32_t forSocketID) { + auto it = connections_.find(static_cast(forSocketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to send binary data to a socket (" << forSocketID + << "), the socket is not open."; + return; + } + std::string decoded; + decoded.reserve(base64String.size() * 3 / 4); + uint32_t value = 0; + int bits = -8; + for (char ch : base64String) { + if (ch == '=') break; + const std::string_view alphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const auto index = alphabet.find(ch); + if (index == std::string_view::npos) continue; + value = (value << 6) | static_cast(index); + bits += 6; + if (bits >= 0) { + decoded.push_back(static_cast((value >> bits) & 0xff)); + bits -= 8; + } + } + it->second->sendBinary(decoded); +} + +void WebSocketModule::ping(jsi::Runtime& /*rt*/, int32_t socketID) { + auto it = connections_.find(static_cast(socketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to ping a socket (" << socketID + << "), the socket is not open."; + return; + } + it->second->ping(); +} + +void WebSocketModule::close( + jsi::Runtime& /*rt*/, + int32_t code, + const std::string& reason, + int32_t socketID) { + auto it = connections_.find(static_cast(socketID)); + if (it == connections_.end()) { + LOG(ERROR) << "Failed to close a socket (" << socketID + << "), the socket is not open."; + return; + } + it->second->close(static_cast(code), reason); + removeSocketSender(socketID); + setBlobWebSocketHandler(socketID, false); + connections_.erase(it); +} + +void WebSocketModule::addListener( + jsi::Runtime& /*rt*/, + const std::string& /*eventName*/) {} + +void WebSocketModule::removeListeners(jsi::Runtime& /*rt*/, int32_t /*count*/) { +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/WebSocketModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/WebSocketModule.h new file mode 100644 index 0000000000..8ade7372df --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/WebSocketModule.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class IWebSocketClient; + +class WebSocketModule : public NativeWebSocketModuleCxxSpec, + public std::enable_shared_from_this { + public: + WebSocketModule(std::shared_ptr jsInvoker, WebSocketClientFactory webSocketClientFactory); + ~WebSocketModule() override; + WebSocketModule(const WebSocketModule &other) = delete; + WebSocketModule &operator=(WebSocketModule &other) = delete; + WebSocketModule(WebSocketModule &&other) = delete; + WebSocketModule &operator=(WebSocketModule &&other) = delete; + + void connect( + jsi::Runtime &rt, + const std::string &url, + const std::optional> &protocols, + jsi::Object options, + int32_t socketID); + + void send(jsi::Runtime &rt, const std::string &message, int32_t socketID); + + void sendBinary(jsi::Runtime &rt, const std::string &base64String, int32_t socketID); + + void ping(jsi::Runtime &rt, int32_t socketID); + + void close(jsi::Runtime &rt, int32_t code, const std::string &reason, int32_t socketID); + + void addListener(jsi::Runtime &rt, const std::string &eventName); + + void removeListeners(jsi::Runtime &rt, int32_t count); + + private: + WebSocketClientFactory webSocketClientFactory_; + std::unordered_map> connections_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp new file mode 100644 index 0000000000..8fd3a182d4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "AssetManagerHelpers.h" + +#include +#include + +namespace facebook::react { + +jobject getAssetManagerObject(JNIEnv* env) { + auto contextObject = getApplication(env); + auto contextClass = env->GetObjectClass(contextObject); + auto getAssetsMethod = env->GetMethodID( + contextClass, "getAssets", "()Landroid/content/res/AssetManager;"); + return env->CallObjectMethod(contextObject, getAssetsMethod); +} + +AAssetManager* getJavaAssetManager() { + auto env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + auto AssetManagerObject = getAssetManagerObject(env); + return AAssetManager_fromJava(env, AssetManagerObject); +} + +bool isDirectoryNotEmpty(const std::string& path) { + auto env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + auto AssetManagerObject = getAssetManagerObject(env); + auto listMethodID = env->GetMethodID( + env->GetObjectClass(AssetManagerObject), + "list", + "(Ljava/lang/String;)[Ljava/lang/String;"); + + auto pathString = env->NewStringUTF(path.c_str()); + auto filesObject = static_cast( + env->CallObjectMethod(AssetManagerObject, listMethodID, pathString)); + env->DeleteLocalRef(pathString); + + auto length = env->GetArrayLength(filesObject); + for (int i = 0; i < length; i++) { + auto jstr = + static_cast(env->GetObjectArrayElement(filesObject, i)); + + const char* filename = env->GetStringUTFChars(jstr, nullptr); + + if (filename != nullptr) { + env->ReleaseStringUTFChars(jstr, filename); + env->DeleteLocalRef(jstr); + return true; + } + + env->DeleteLocalRef(jstr); + } + return false; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h new file mode 100644 index 0000000000..8df2d98ca9 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/AssetManagerHelpers.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +AAssetManager *getJavaAssetManager(); + +bool isDirectoryNotEmpty(const std::string &path); + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp new file mode 100644 index 0000000000..616a23a281 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/android/ResourceLoader.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ResourceLoader.h" +#include "AssetManagerHelpers.h" + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { + +AAssetManager* assetManager_ = nullptr; + +AAssetManager* getAssetManager() { + if (assetManager_ == nullptr) { + assetManager_ = getJavaAssetManager(); + } + + return assetManager_; +} +} // namespace + +bool ResourceLoader::isResourceDirectory(const std::string& path) { + auto assetDir = AAssetManager_openDir(getAssetManager(), path.c_str()); + if (assetDir == nullptr) { + return false; + } + + bool exists = AAssetDir_getNextFileName(assetDir) != nullptr; + AAssetDir_close(assetDir); + if (exists) { + return true; + } + return isDirectoryNotEmpty(path); +} + +bool ResourceLoader::isResourceFile(const std::string& path) { + auto asset = AAssetManager_open( + getAssetManager(), path.c_str(), AASSET_MODE_STREAMING); + if (asset == nullptr) { + return false; + } + + AAsset_close(asset); + return true; +} + +std::unique_ptr ResourceLoader::getResourceFileContents( + const std::string& path) { + return loadScriptFromAssets(getAssetManager(), path); +} + +std::filesystem::path ResourceLoader::getCacheRootPath() { + return getContext()->getCacheDir()->getAbsolutePath(); +} +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp new file mode 100644 index 0000000000..2ff6f35455 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/platform/cxx/ResourceLoader.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include + +#include + +namespace facebook::react { + +bool ResourceLoader::isResourceDirectory(const std::string& path) { + return std::filesystem::is_directory(path); +} + +bool ResourceLoader::isResourceFile(const std::string& path) { + return std::filesystem::exists(path) && !std::filesystem::is_directory(path); +} + +/* static */ std::unique_ptr +ResourceLoader::getResourceFileContents(const std::string& path) { + return JSBigFileString::fromPath(path); +} + +/* static */ std::filesystem::path ResourceLoader::getCacheRootPath() { + return std::filesystem::temp_directory_path(); +} +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp new file mode 100644 index 0000000000..a75cc58013 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/tests/NetworkingModuleTests.cpp @@ -0,0 +1,115 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#ifdef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "TestJsRuntime.h" + +namespace facebook::react { + +class NetworkingModuleTests : public testing::Test { + protected: + void SetUp() override { + // The engine comes from the test binary's registration, not from this + // file, so the test only needs a JSI runtime. + runtime_ = tests::createTestRuntimeFromActiveEngine(); + jsInvoker_ = std::make_shared(runtime_->getRuntime()); + } + + static void verifyFormData( + const http::FormDataField& formData, + const std::string& fieldName, + const std::string& string, + std::vector headerValues) { + EXPECT_EQ(formData.fieldName, fieldName); + EXPECT_EQ(formData.string, string); + for (int i = 0; i * 2 < headerValues.size(); ++i) { + EXPECT_EQ(formData.headers[i].first, headerValues[i * 2]); + EXPECT_EQ(formData.headers[i].second, headerValues[i * 2 + 1]); + } + } + + std::unique_ptr runtime_; + std::shared_ptr jsInvoker_; +}; + +// Test parsing a body with form data +TEST_F(NetworkingModuleTests, formDataTest) { + auto dynamic = folly::parseJson( + R"({ + "formData":[ + { + "fieldName":"field1", + "headers":[["header1","form-data; name=\"header1\""]], + "string":"string1" + }, + { + "fieldName":"field2", + "headers":[ + ["header1","form-data; name=\"header1\""], + ["header2","form-data; name=\"header2\""] + ], + "string":"string2" + } + ]})"); + auto jsiValue = jsi::valueFromDynamic(*rt_, dynamic); + auto body = bridging::fromJs(*rt_, jsiValue, jsInvoker_); + + EXPECT_TRUE(body.formData.has_value()); + + EXPECT_FALSE(body.base64.has_value()); + EXPECT_FALSE(body.blob.has_value()); + EXPECT_FALSE(body.string.has_value()); + + auto formData = body.formData.value(); + EXPECT_EQ(formData.size(), 2); + verifyFormData( + formData[0], + "field1", + "string1", + {"header1", "form-data; name=\"header1\""}); + verifyFormData( + formData[1], + "field2", + "string2", + {"header1", + "form-data; name=\"header1\"", + "header2", + "form-data; name=\"header2\""}); +} + +// Test parsing a body with string data +TEST_F(NetworkingModuleTests, stringDataTest) { + auto dynamic = folly::parseJson( + R"({ + "string": "testString" + })"); + auto jsiValue = jsi::valueFromDynamic(*rt_, dynamic); + auto body = bridging::fromJs(*rt_, jsiValue, jsInvoker_); + + EXPECT_TRUE(body.string.has_value()); + + EXPECT_FALSE(body.base64.has_value()); + EXPECT_FALSE(body.blob.has_value()); + EXPECT_FALSE(body.formData.has_value()); + + auto stringData = body.string.value(); + EXPECT_EQ(stringData, "testString"); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/tests/TestJsRuntime.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/tests/TestJsRuntime.h new file mode 100644 index 0000000000..032b2c8fe6 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/io/tests/TestJsRuntime.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "GpuiJsEngine.h" + +#include +#include + +#include +#include + +namespace facebook::react::tests { + +/// Creates the JSI runtime a test should use from `factory` — the same seam +/// ReactHost uses for every (re)load. Tests take a factory instead of +/// constructing a VM of their own, so no test depends on an engine. +inline std::unique_ptr createTestRuntime( + JSRuntimeFactory& factory, + std::shared_ptr messageQueueThread = nullptr) { + return factory.createJSRuntime(std::move(messageQueueThread)); +} + +/// The same runtime, built from the engine the test binary registered. A test +/// binary registers an engine package exactly like an app does (see +/// GpuiJsEngine.h); without one this reports the registry's error. +inline std::unique_ptr createTestRuntimeFromActiveEngine( + std::shared_ptr messageQueueThread = nullptr) { + return createTestRuntime( + *rngpui::activeJsEngine().makeRuntimeFactory(), + std::move(messageQueueThread)); +} + +} // namespace facebook::react::tests diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/jni/JniHelper.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/jni/JniHelper.cpp new file mode 100644 index 0000000000..3799ed893a --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/jni/JniHelper.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "JniHelper.h" + +#include +#include + +namespace facebook::react { + +jobject getApplication(JNIEnv* env) { + auto activityThreadClass = env->FindClass("android/app/ActivityThread"); + auto currentApplicationMethodID = env->GetStaticMethodID( + activityThreadClass, "currentApplication", "()Landroid/app/Application;"); + return env->CallStaticObjectMethod( + activityThreadClass, currentApplicationMethodID); +} + +jni::alias_ref getContext() { + auto env = facebook::jni::Environment::ensureCurrentThreadIsAttached(); + auto application = getApplication(env); + return facebook::jni::wrap_alias( + static_cast(application)); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/jni/JniHelper.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/jni/JniHelper.h new file mode 100644 index 0000000000..b449ca2540 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/jni/JniHelper.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +jobject getApplication(JNIEnv *env); + +jni::alias_ref getContext(); + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/CMakeLists.txt new file mode 100644 index 0000000000..3aa3e63bf5 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_logging_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_cxx_platform_react_logging OBJECT ${react_cxx_platform_react_logging_SRC}) + +target_include_directories(react_cxx_platform_react_logging PUBLIC ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_logging + glog + react_codegen_rncore + jserrorhandler + react_bridging + logger +) +target_compile_reactnative_options(react_cxx_platform_react_logging PRIVATE) +target_compile_options(react_cxx_platform_react_logging PRIVATE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultLogger.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultLogger.cpp new file mode 100644 index 0000000000..4e2d9e3dc4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultLogger.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "DefaultLogger.h" + +#include +#include +#include + +namespace facebook::react { + +Logger getDefaultLogger() { + return [](const std::string& message, unsigned int logLevel) { + static std::string tag = "[JavaScript]: "; + switch (logLevel) { + case ReactNativeLogLevelFatal: + case ReactNativeLogLevelError: + LOG(ERROR) << tag << message; + break; + case ReactNativeLogLevelWarning: + LOG(WARNING) << tag << message; + break; + case ReactNativeLogLevelInfo: + default: + LOG(INFO) << tag << message; + break; + } + }; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultLogger.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultLogger.h new file mode 100644 index 0000000000..d887dd168b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultLogger.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +using Logger = std::function; + +Logger getDefaultLogger(); + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultOnJsErrorHandler.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultOnJsErrorHandler.h new file mode 100644 index 0000000000..d512d2b006 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/DefaultOnJsErrorHandler.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +static inline JsErrorHandler::OnJsError getDefaultOnJsErrorFunc() +{ + return [](jsi::Runtime & /*runtime*/, const JsErrorHandler::ProcessedError &error) { + LOG(ERROR) << "[onJsError called]: " << error << std::endl; + }; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/LogOnce.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/LogOnce.cpp new file mode 100644 index 0000000000..c35fa9b342 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/LogOnce.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "LogOnce.h" + +#include + +#include + +namespace facebook::react { + +LogOnceWrapper::~LogOnceWrapper() { + std::string message = str(); + + static std::unordered_set loggedMessages; + if (loggedMessages.find(message) != loggedMessages.end()) { + return; + } + + switch (severity_) { + case Severity::Info: + LOG(INFO) << message; + break; + case Severity::Warning: + LOG(WARNING) << message; + break; + case Severity::Error: + LOG(ERROR) << message; + break; + case Severity::Fatal: + default: + LOG(FATAL) << message; + break; + } + + loggedMessages.insert(message); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/LogOnce.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/LogOnce.h new file mode 100644 index 0000000000..ad66d1f21b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/LogOnce.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include + +#define LOG_INFO_ONCE LogOnceWrapper(LogOnceWrapper::Severity::Info) +#define LOG_WARNING_ONCE LogOnceWrapper(LogOnceWrapper::Severity::Warning) +#define LOG_ERROR_ONCE LogOnceWrapper(LogOnceWrapper::Severity::Error) +#define LOG_FATAL_ONCE LogOnceWrapper(LogOnceWrapper::Severity::Fatal) + +namespace facebook::react { + +class LogOnceWrapper : public std::stringstream { + public: + enum class Severity { Info, Warning, Error, Fatal }; + + LogOnceWrapper(Severity severity = Severity::Info) : severity_(severity) {} + + ~LogOnceWrapper(); + + private: + Severity severity_{Severity::Info}; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/NativeExceptionsManager.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/NativeExceptionsManager.cpp new file mode 100644 index 0000000000..ef920df59f --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/NativeExceptionsManager.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "NativeExceptionsManager.h" + +#include + +namespace facebook::react { + +void NativeExceptionsManager::reportFatalException( + jsi::Runtime& rt, + std::string message, + std::vector stack, + int32_t exceptionId) { + reportException( + rt, + ExceptionData{ + .message = std::move(message), + .stack = std::move(stack), + .id = exceptionId, + .isFatal = true}); +} + +void NativeExceptionsManager::reportSoftException( + jsi::Runtime& rt, + std::string message, + std::vector stack, + int32_t exceptionId) { + reportException( + rt, + ExceptionData{ + .message = std::move(message), + .stack = std::move(stack), + .id = exceptionId, + .isFatal = false}); +} + +void NativeExceptionsManager::reportException( + jsi::Runtime& runtime, + const ExceptionData& data) { + if (onJsError_) { + std::vector frames; + frames.reserve(data.stack.size()); + for (const auto& frame : data.stack) { + frames.push_back( + JsErrorHandler::ProcessedError::StackFrame{ + .file = frame.file, + .methodName = frame.methodName, + .lineNumber = frame.lineNumber, + .column = frame.column}); + } + JsErrorHandler::ProcessedError processedError{ + .message = data.message, + .originalMessage = std::nullopt, + .name = std::nullopt, + .componentStack = std::nullopt, + .stack = frames, + .id = data.id, + .isFatal = data.isFatal, + .extraData = jsi::Object(runtime), + }; + onJsError_(runtime, processedError); + } +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/NativeExceptionsManager.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/NativeExceptionsManager.h new file mode 100644 index 0000000000..4dba94aa3f --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/logging/NativeExceptionsManager.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +// https://github.com/facebook/react-native/blob/v0.71.1/Libraries/Core/NativeExceptionsManager.js#L17-L35 +using StackFrame = NativeExceptionsManagerStackFrame< + std::optional, + std::optional, + std::optional, + std::string, + std::optional>; + +template <> +struct Bridging : NativeExceptionsManagerStackFrameBridging {}; + +using ExceptionData = NativeExceptionsManagerExceptionData< + std::string, + std::optional, + std::optional, + std::optional, + std::vector, + int32_t, + bool, + std::optional>>; + +template <> +struct Bridging : NativeExceptionsManagerExceptionDataBridging {}; + +class NativeExceptionsManager : public NativeExceptionsManagerCxxSpec { + public: + NativeExceptionsManager(JsErrorHandler::OnJsError onJsError, std::shared_ptr jsInvoker) + : NativeExceptionsManagerCxxSpec(jsInvoker), onJsError_(std::move(onJsError)) + { + } + + void reportFatalException(jsi::Runtime &rt, std::string message, std::vector stack, int32_t exceptionId); + + void reportSoftException(jsi::Runtime &rt, std::string message, std::vector stack, int32_t exceptionId); + + void reportException(jsi::Runtime &runtime, const ExceptionData &data); + + void updateExceptionMessage(jsi::Runtime &rt, jsi::String message, jsi::Array stack, double exceptionId) + { + // This method is not in modern React Native: + // https://github.com/search?q=repo%3Afacebook%2Freact-native+updateExceptionMessage&type=code + } + + void dismissRedbox(jsi::Runtime &rt) + { + // Redbox is not supported at this time, LogBox is: + // https://reactnative.dev/blog/2020/07/06/version-0.63 + } + + private: + JsErrorHandler::OnJsError onJsError_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/CMakeLists.txt new file mode 100644 index 0000000000..e71e8d17c2 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/CMakeLists.txt @@ -0,0 +1,20 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +add_library(react_cxx_platform_react_nativemodule INTERFACE) + +target_include_directories(react_cxx_platform_react_nativemodule INTERFACE ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_nativemodule INTERFACE + folly_runtime + react_nativemodule_core +) +target_compile_reactnative_options(react_cxx_platform_react_nativemodule INTERFACE) +target_compile_options(react_cxx_platform_react_nativemodule INTERFACE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/JavaScriptModule.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/JavaScriptModule.h new file mode 100644 index 0000000000..5b038a00c2 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/JavaScriptModule.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +using JavaScriptModuleCallback = + std::function; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/TurboModuleProvider.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/TurboModuleProvider.h new file mode 100644 index 0000000000..b10c248aab --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/nativemodule/TurboModuleProvider.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class CallInvoker; +class TurboModule; + +using TurboModuleProvider = + std::function(const std::string &name, const std::shared_ptr &jsInvoker)>; + +using TurboModuleProviders = std::vector; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/CMakeLists.txt new file mode 100644 index 0000000000..61e5cd07c4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_profiling_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_cxx_platform_react_profiling OBJECT ${react_cxx_platform_react_profiling_SRC}) + +target_include_directories(react_cxx_platform_react_profiling PUBLIC ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_profiling + fmt + glog + jsi +) +target_compile_reactnative_options(react_cxx_platform_react_profiling PRIVATE) +target_compile_options(react_cxx_platform_react_profiling PRIVATE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/FrameStats.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/FrameStats.cpp new file mode 100644 index 0000000000..576332c095 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/FrameStats.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "FrameStats.h" +#include "TimeSeries.h" + +#include + +#include + +namespace facebook::react { + +static constexpr double FRAME_STATS_LOGGING_INTERVAL_MS = 1000.0; +static constexpr int FRAME_STATS_SERIES_CAPACITY = 256; + +struct FrameStatsLogger { + bool enablePrinting{false}; + double lastLoggedTimeMs{0.0}; + TimeSeries frameTimeSeries{FRAME_STATS_SERIES_CAPACITY}; +}; + +static FrameStatsLogger& getFrameStatsLogger() { + static FrameStatsLogger s_FrameStatsLogger; + return s_FrameStatsLogger; +} + +void enableFrameStatsPrinting(bool enable) { + getFrameStatsLogger().enablePrinting = enable; +} +void logFrameStats(double timeStampMs, double durationMs) { + auto& logger = getFrameStatsLogger(); + logger.frameTimeSeries.appendValue(timeStampMs, durationMs); + + if (logger.lastLoggedTimeMs == 0.0) { + logger.lastLoggedTimeMs = timeStampMs; + } + + if (logger.enablePrinting && + timeStampMs >= + logger.lastLoggedTimeMs + FRAME_STATS_LOGGING_INTERVAL_MS) { + const auto& timeSeries = logger.frameTimeSeries; + const auto startTime = timeStampMs - FRAME_STATS_LOGGING_INTERVAL_MS; + const auto endTime = timeStampMs; + + const int fps = timeSeries.getCount(startTime, endTime) * 1000.0 / + FRAME_STATS_LOGGING_INTERVAL_MS; + + const auto frameTimeAvg = timeSeries.getAverage(startTime, endTime); + const auto frameTimeMax = timeSeries.getMax(startTime, endTime); + const auto frameTimeMin = timeSeries.getMin(startTime, endTime); + const auto frameTimeP50 = timeSeries.getPercentile(50, startTime, endTime); + const auto frameTimeP75 = timeSeries.getPercentile(75, startTime, endTime); + const auto frameTimeP95 = timeSeries.getPercentile(95, startTime, endTime); + + LOG(INFO) << std::fixed << std::setprecision(2) << std::setfill(' ') + << std::left << "[FRAME STATS] FPS=" << std::setw(2) << fps + << " frame(ms): avg=" << std::setw(5) << std::left << frameTimeAvg + << " min=" << std::setw(5) << std::left << frameTimeMin + << " p50=" << std::setw(5) << std::left << frameTimeP50 + << " p75=" << std::setw(5) << std::left << frameTimeP75 + << " p95=" << std::setw(5) << std::left << frameTimeP95 + << " max=" << std::setw(5) << std::left << frameTimeMax; + + logger.lastLoggedTimeMs = timeStampMs; + } +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/FrameStats.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/FrameStats.h new file mode 100644 index 0000000000..ec2d5340c3 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/FrameStats.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include "perfetto.h" + +#define RENDER_FRAME_EVENT() \ + TRACE_EVENT("rncxx", "render"); \ + facebook::react::FrameStatsBlock __frameStatsBlock_##__LINE__; + +#define PROCESS_FRAME_EVENT() \ + TRACE_EVENT("rncxx", "frame"); \ + facebook::react::FrameStatsBlock __frameStatsBlock_##__LINE__; + +namespace facebook::react { +class TimeSeries; + +void enableFrameStatsPrinting(bool enable = true); +void logFrameStats(double timeStampMs, double durationMs); + +inline double getTimeStampMs() +{ + return std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()) + .count() / + 1000000.0; +} + +class FrameStatsBlock { + public: + FrameStatsBlock() : startTimeStamp_(getTimeStampMs()) {} + + ~FrameStatsBlock() + { + logFrameStats(startTimeStamp_, getTimeStampMs() - startTimeStamp_); + } + + private: + double startTimeStamp_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/RuntimeHeapInfoCollector.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/RuntimeHeapInfoCollector.cpp new file mode 100644 index 0000000000..c838515c88 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/RuntimeHeapInfoCollector.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "RuntimeHeapInfoCollector.h" +#include +#include +#include +#include + +namespace facebook::react { + +RuntimeHeapInfoCollector::RuntimeHeapInfoCollector( + std::chrono::milliseconds intervalMs) + : intervalMs_(intervalMs) { + if (intervalMs_ <= std::chrono::milliseconds::zero()) { + throw std::runtime_error( + fmt::format( + "Invalid interval, must be > 0ms, got {} ms", intervalMs_.count())); + } + + lastCollectionTime_ = std::chrono::steady_clock::now(); +} + +std::unique_ptr RuntimeHeapInfoCollector::create( + std::chrono::milliseconds intervalMs) { + if (intervalMs <= std::chrono::milliseconds::zero()) { + LOG(INFO) << "RuntimeHeapInfoCollector is disabled"; + return nullptr; + } + + return std::make_unique(intervalMs); +} + +void RuntimeHeapInfoCollector::collectHeapInfo(jsi::Runtime& runtime) { + auto now = std::chrono::steady_clock::now(); + if (now - lastCollectionTime_ < intervalMs_) { + return; + } + + lastCollectionTime_ = now; + auto heap = runtime.instrumentation().getHeapInfo(false); + heapInfo_ = { + .allocatedBytes = heap["hermes_allocatedBytes"], + .heapSize = heap["hermes_heapSize"], + .numCollections = heap["hermes_numCollections"], + }; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/RuntimeHeapInfoCollector.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/RuntimeHeapInfoCollector.h new file mode 100644 index 0000000000..120c394fa7 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/RuntimeHeapInfoCollector.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +struct HeapInfo { + int64_t allocatedBytes{0}; + int64_t heapSize{0}; + int64_t numCollections{0}; +}; + +inline std::ostream &operator<<(std::ostream &os, HeapInfo &heapInfo) +{ + os << "[heapInfo: allocatedBytes: " << heapInfo.allocatedBytes << ", heapSize: " << heapInfo.heapSize + << ", numCollections: " << heapInfo.numCollections << "]"; + return os; +} + +class RuntimeHeapInfoCollector { + public: + RuntimeHeapInfoCollector(std::chrono::milliseconds intervalMs); + + static std::unique_ptr create(std::chrono::milliseconds intervalMs); + + void collectHeapInfo(jsi::Runtime &runtime); + + HeapInfo getHeapInfo() + { + return heapInfo_; + } + + private: + HeapInfo heapInfo_{}; + std::chrono::milliseconds intervalMs_; + std::chrono::steady_clock::time_point lastCollectionTime_; +}; +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/TimeSeries.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/TimeSeries.cpp new file mode 100644 index 0000000000..ee01167018 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/TimeSeries.cpp @@ -0,0 +1,330 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "TimeSeries.h" + +#include +#include + +namespace facebook::react { + +TimeSeries::TimeSeries(int capacity) { + setCapacity(capacity); +} + +bool TimeSeries::operator==(const TimeSeries& rhs) const { + const int n = getNumPoints(); + if (n != rhs.getNumPoints()) { + return false; + } + + for (int i = 0; i < n; i++) { + if (timeAtIndex(i) != rhs.timeAtIndex(i) || + valueAtIndex(i) != rhs.valueAtIndex(i)) { + return false; + } + } + return true; +} + +void TimeSeries::setCapacity(int capacity) { + // Could have preserved the data, but don't bother for now + times_ = std::vector(); + times_.reserve(capacity); + + values_ = std::vector(); + values_.reserve(capacity); +} + +void TimeSeries::reset() { + times_ = {}; + values_ = {}; + position_ = 0; +} + +int TimeSeries::findHistoryPointIndex(double t, TimeSeries::Bound bound) const { + if (times_.empty()) { + return -1; + } + if (t >= times_[0]) { + auto it = (bound == Bound::Lower) + ? std::lower_bound(times_.begin(), times_.begin() + position_, t) + : std::upper_bound(times_.begin(), times_.begin() + position_, t); + return std::min( + static_cast(it - times_.begin()), static_cast(position_ - 1)); + } else if (position_ < times_.size()) { + auto it = (bound == Bound::Lower) + ? std::lower_bound(times_.begin() + position_, times_.end(), t) + : std::upper_bound(times_.begin() + position_, times_.end(), t); + return static_cast(it - times_.begin()); + } else { + return 0; + } +} + +TimeSeries::Range TimeSeries::findHistoryPointRange( + double timeFrom, + double timeTo) const { + Range res; + if (times_.empty() || timeTo < timeFrom) { + return res; + } + const int position = static_cast(position_); + const int numPoints = static_cast(times_.size()); + + // index of time point greater or equal to timeFrom + int idxL = findHistoryPointIndex(timeFrom, Bound::Lower); + if (idxL == position - 1 && times_[idxL] < timeFrom) { + idxL++; + } + + // index of time point lower or equal to timeTo + int idxR = findHistoryPointIndex(timeTo, Bound::Upper); + if (idxR < numPoints && times_[idxR] <= timeTo) { + idxR++; + } + + const bool fullRange = timeFrom <= getMinTime() && timeTo >= getMaxTime(); + if (fullRange) { + if (position < numPoints) { + res.idxFrom1 = position; + res.idxTo1 = numPoints; + res.idxFrom2 = 0; + res.idxTo2 = position; + } else { + res.idxFrom1 = 0; + res.idxTo1 = numPoints; + res.idxFrom2 = 0; + res.idxTo2 = 0; + } + } else if (idxL >= numPoints) { + res.idxFrom1 = 0; + res.idxTo1 = idxR; + res.idxFrom2 = 0; + res.idxTo2 = 0; + } else if (idxL > idxR) { + res.idxFrom1 = idxL; + res.idxTo1 = numPoints; + res.idxFrom2 = 0; + res.idxTo2 = idxR; + } else { + res.idxFrom1 = idxL; + res.idxTo1 = idxR; + res.idxFrom2 = 0; + res.idxTo2 = 0; + } + + return res; +} + +TimeSeries::Range TimeSeries::wholeRange() const { + Range res; + const int position = static_cast(position_); + const int numPoints = static_cast(times_.size()); + + if (position >= numPoints) { + res.idxFrom1 = 0; + res.idxTo1 = position; + res.idxFrom2 = 0; + res.idxTo2 = 0; + } else { + res.idxFrom1 = position; + res.idxTo1 = numPoints; + res.idxFrom2 = 0; + res.idxTo2 = position; + } + return res; +} + +void TimeSeries::appendValue(double t, double value) { + if (getMaxTime() > t) { + // ignore old values + return; + } + if (!times_.empty() && getMaxTime() == t) { + values_[values_.size() - 1] = value; + } else if (times_.size() < times_.capacity()) { + times_.push_back(t); + values_.push_back(value); + position_ = times_.size(); + } else { + position_ = position_ % times_.size(); + times_[position_] = t; + values_[position_] = value; + position_++; + } +} + +void TimeSeries::accumulateValue(double timeFrom, double timeTo, double value) { + if (timeTo - timeFrom <= 0.0) { + return; + } + + if (getNumPoints() == 0 || timeFrom >= getMaxTime()) { + appendValue(timeFrom, 0.0); + appendValue(timeTo, value); + return; + } + + double curTime = timeFrom; + const int n = getNumPoints(); + int curPoint = + (findHistoryPointIndex(timeFrom, Bound::Upper) + n - position_) % n; + + while (curPoint < n && timeAtIndex(curPoint) < timeTo) { + const double t = timeAtIndex(curPoint); + valueAtIndex(curPoint) += value * (t - curTime) / (timeTo - timeFrom); + curTime = t; + curPoint++; + } + + if (curTime < timeTo) { + const double dVal = value * (timeTo - curTime) / (timeTo - timeFrom); + if (curPoint == n) { + appendValue(timeTo, dVal); + } else { + valueAtIndex(curPoint) += dVal; + } + } +} + +double TimeSeries::getPercentile( + unsigned int percentile, + const TimeSeries::Range& range) const { + if (range.isEmpty()) { + return 0.0f; + } + std::vector values(range.size()); + std::copy( + values_.begin() + range.idxFrom1, + values_.begin() + range.idxTo1, + values.begin()); + std::copy( + values_.begin() + range.idxFrom2, + values_.begin() + range.idxTo2, + values.begin() + (range.idxTo1 - range.idxFrom1)); + + const size_t k = percentile * values.size() / 100; + std::nth_element(values.begin(), values.begin() + k, values.end()); + return *(values.begin() + k); +} + +double TimeSeries::getSum(const TimeSeries::Range& range) const { + if (range.isEmpty()) { + return 0.0f; + } + double sum = std::accumulate( + values_.begin() + range.idxFrom1, values_.begin() + range.idxTo1, 0.0); + sum = std::accumulate( + values_.begin() + range.idxFrom2, values_.begin() + range.idxTo2, sum); + return sum; +} + +double TimeSeries::getAverage(const TimeSeries::Range& range) const { + if (range.isEmpty()) { + return 0.0f; + } + return getSum(range) / range.size(); +} + +double TimeSeries::getMax(const TimeSeries::Range& range) const { + if (range.isEmpty()) { + return 0.0f; + } + const double v1 = *std::max_element( + values_.begin() + range.idxFrom1, values_.begin() + range.idxTo1); + const double v2 = range.isContinuous() + ? v1 + : *std::max_element( + values_.begin() + range.idxFrom2, values_.begin() + range.idxTo2); + return std::max(v1, v2); +} + +double TimeSeries::getMin(const TimeSeries::Range& range) const { + if (range.isEmpty()) { + return 0.0f; + } + const double v1 = *std::min_element( + values_.begin() + range.idxFrom1, values_.begin() + range.idxTo1); + const double v2 = range.isContinuous() + ? v1 + : *std::min_element( + values_.begin() + range.idxFrom2, values_.begin() + range.idxTo2); + return std::min(v1, v2); +} + +double TimeSeries::getPercentile(unsigned int percentile) const { + return getPercentile(percentile, wholeRange()); +} + +double TimeSeries::getPercentile( + unsigned int percentile, + double timeFrom, + double timeTo) const { + return getPercentile(percentile, findHistoryPointRange(timeFrom, timeTo)); +} + +double TimeSeries::getAverage() const { + return getAverage(wholeRange()); +} + +double TimeSeries::getAverage(double timeFrom, double timeTo) const { + return getAverage(findHistoryPointRange(timeFrom, timeTo)); +} + +double TimeSeries::getMax() const { + return getMax(wholeRange()); +} + +double TimeSeries::getMax(double timeFrom, double timeTo) const { + return getMax(findHistoryPointRange(timeFrom, timeTo)); +} + +double TimeSeries::getMin() const { + return getMin(wholeRange()); +} + +double TimeSeries::getMin(double timeFrom, double timeTo) const { + return getMin(findHistoryPointRange(timeFrom, timeTo)); +} + +double TimeSeries::getSum() const { + return getSum(wholeRange()); +} + +double TimeSeries::getSum(double timeFrom, double timeTo) const { + return getSum(findHistoryPointRange(timeFrom, timeTo)); +} + +int TimeSeries::getCount(double timeFrom, double timeTo) const { + return findHistoryPointRange(timeFrom, timeTo).size(); +} + +double TimeSeries::getMaxTime() const { + if (times_.empty()) { + return 0.0; + } + return times_[position_ - 1]; +} + +double TimeSeries::getMinTime() const { + if (times_.empty()) { + return 0.0; + } + return times_[position_ % times_.size()]; +} + +double TimeSeries::getValue(double t) const { + if (times_.empty()) { + return 0.0; + } + int pidx = findHistoryPointIndex(t); + pidx = std::min(std::max(pidx, 0), static_cast(times_.size() - 1)); + return values_[pidx]; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/TimeSeries.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/TimeSeries.h new file mode 100644 index 0000000000..1e2ba3669d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/TimeSeries.h @@ -0,0 +1,131 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class TimeSeries { + // A "compound range" structure to account for wrapped around intervals + struct Range { + int idxFrom1{0}; + int idxTo1{0}; + int idxFrom2{0}; + int idxTo2{0}; + + int size() const + { + return (idxTo1 - idxFrom1) + (idxTo2 - idxFrom2); + } + + bool isEmpty() const + { + return size() == 0; + } + + bool isContinuous() const + { + return idxTo2 - idxFrom2 == 0; + } + }; + + public: + static constexpr int DEFAULT_CAPACITY = 100; + enum class Bound { Upper = 0, Lower = 1 }; + + TimeSeries(int capacity = DEFAULT_CAPACITY); + + bool operator==(const TimeSeries &rhs) const; + + int getCapacity() const + { + return static_cast(times_.capacity()); + } + void setCapacity(int capacity); + + void reset(); + + int findHistoryPointIndex(double t, Bound bound = Bound::Lower) const; + + int getNumPoints() const + { + return static_cast(times_.size()); + } + + double &valueAtIndex(int idx) + { + const int numPoints = getNumPoints(); + return values_[(idx + position_) % numPoints]; + } + + double &timeAtIndex(int idx) + { + const int numPoints = getNumPoints(); + return times_[(idx + position_) % numPoints]; + } + + double valueAtIndex(int idx) const + { + const int numPoints = getNumPoints(); + return values_[(idx + position_) % numPoints]; + } + + double timeAtIndex(int idx) const + { + const int numPoints = getNumPoints(); + return times_[(idx + position_) % numPoints]; + } + + void appendValue(double time, double value); + + void accumulateValue(double timeFrom, double timeTo, double value); + + double getValue(double time) const; + + double getPercentile(unsigned int percentile) const; + double getPercentile(unsigned int percentile, double timeFrom, double timeTo) const; + + double getAverage() const; + double getAverage(double timeFrom, double timeTo) const; + + double getMax() const; + double getMax(double timeFrom, double timeTo) const; + + double getMin() const; + double getMin(double timeFrom, double timeTo) const; + + double getSum() const; + double getSum(double timeFrom, double timeTo) const; + + int getCount(double timeFrom, double timeTo) const; + + double getMaxTime() const; + double getMinTime() const; + + friend std::ostream &operator<<(std::ostream &os, const TimeSeries &ts); + friend std::ostream &operator<<(std::ostream &os, const TimeSeries::Range &range); + + private: + std::vector times_; + std::vector values_; + + size_t position_{0}; + + Range findHistoryPointRange(double timeFrom, double timeTo) const; + Range wholeRange() const; + + double getPercentile(unsigned int percentile, const Range &range) const; + double getAverage(const Range &range) const; + double getMax(const Range &range) const; + double getMin(const Range &range) const; + double getSum(const Range &range) const; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/Tracing.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/Tracing.cpp new file mode 100644 index 0000000000..88e36158e2 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/Tracing.cpp @@ -0,0 +1,12 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "tracy_noop.h" + +#if RNCXX_WITH_PROFILING_PROVIDER +HZT_DEFINE_STATIC_STORAGE(facebook::react); +#endif diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/perfetto.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/perfetto.cpp new file mode 100644 index 0000000000..c15a1179ae --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/perfetto.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#if WITH_PERFETTO +#include + +void initializePerfetto() { + static std::once_flag s_PerfettoIsInitialized; + + std::call_once(s_PerfettoIsInitialized, []() { + perfetto::TracingInitArgs args; + args.shmem_size_hint_kb = 20 * 1024; +#ifdef ANDROID + args.backends |= perfetto::kSystemBackend; +#else + args.backends |= perfetto::kInProcessBackend; +#endif + args.use_monotonic_clock = true; + perfetto::Tracing::Initialize(args); + facebook::react::TrackEvent::Register(); + }); +} + +#endif // WITH_PERFETTO diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/perfetto.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/perfetto.h new file mode 100644 index 0000000000..ea7fe39a95 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/perfetto.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include + +#ifndef WITH_PERFETTO +#define WITH_PERFETTO 0 +#endif + +#if WITH_PERFETTO +#include + +PERFETTO_DEFINE_CATEGORIES_IN_NAMESPACE( + facebook::react, + perfetto::Category("rncxx").SetDescription("Events from RN/Granite")); + +void initializePerfetto(); + +inline uint64_t getCurrentPerfettoTimestamp() +{ + return std::chrono::duration_cast( + std::chrono::high_resolution_clock::now().time_since_epoch()) + .count(); +} + +#else +#define TRACE_EVENT(category, name, ...) SCOPED_TRACE_CPU((category "_" name)) +#define TRACE_COUNTER(category, name, value, ...) + +inline void initializePerfetto() {} +#endif // WITH_PERFETTO diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/tests/TimeSeriesTests.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/tests/TimeSeriesTests.cpp new file mode 100644 index 0000000000..12888b98ce --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/tests/TimeSeriesTests.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include + +namespace facebook::react { + +class TimeSeriesTests : public testing::Test {}; + +std::ostream& operator<<(std::ostream& os, const TimeSeries& ts) { + const size_t n = ts.getNumPoints(); + os << "Position: " << ts.position_ << ", "; + os << "Points: " << ts.getNumPoints() << ", "; + for (size_t i = 0; i < n; i++) { + os << "[" << ts.times_[i] << ", " << ts.values_[i] << "] "; + } + return os; +} + +std::ostream& operator<<(std::ostream& os, const TimeSeries::Range& range) { + os << " [" << range.idxFrom1 << " " << range.idxTo1 << "] [" << range.idxFrom2 + << " " << range.idxTo2 << "]"; + return os; +} + +TEST_F(TimeSeriesTests, basicOperations) { + TimeSeries ts(5); + + EXPECT_FLOAT_EQ(0.0, ts.getAverage()); + EXPECT_FLOAT_EQ(0.0, ts.getPercentile(75)); + EXPECT_FLOAT_EQ(0.0, ts.getMin()); + EXPECT_FLOAT_EQ(0.0, ts.getMax()); + + ts.appendValue(0.0, 3.0); + EXPECT_FLOAT_EQ(3.0, ts.getValue(0.0)); + + ts.appendValue(1.0, 6.0); + ts.appendValue(3.0, 0.0); + EXPECT_FLOAT_EQ(6.0, ts.getValue(1.0)); + + ts.appendValue(4.0, 10.0); + ts.appendValue(5.0, 1.0); + + EXPECT_FLOAT_EQ(3.0, ts.getValue(-1.0)); + EXPECT_FLOAT_EQ(3.0, ts.getValue(0.0)); + EXPECT_FLOAT_EQ(6.0, ts.getValue(1.0)); + EXPECT_FLOAT_EQ(0.0, ts.getValue(3.0)); + EXPECT_FLOAT_EQ(10.0, ts.getValue(4.0)); + EXPECT_FLOAT_EQ(1.0, ts.getValue(5.0)); + EXPECT_FLOAT_EQ(1.0, ts.getValue(6.0)); + + EXPECT_FLOAT_EQ(4.0, ts.getAverage()); + EXPECT_FLOAT_EQ(3.0, ts.getPercentile(50)); + EXPECT_FLOAT_EQ(0.0, ts.getMin()); + EXPECT_FLOAT_EQ(10.0, ts.getMax()); + + ts.appendValue(6.0, 2.0); + ts.appendValue(10.0, 7.0); + + EXPECT_FLOAT_EQ(10.0, ts.getMaxTime()); + EXPECT_FLOAT_EQ(3.0, ts.getMinTime()); + + EXPECT_FLOAT_EQ(0.0, ts.getValue(-1.0)); + EXPECT_FLOAT_EQ(0.0, ts.getValue(2.0)); + EXPECT_FLOAT_EQ(0.0, ts.getValue(3.0)); + EXPECT_FLOAT_EQ(10.0, ts.getValue(4.0)); + EXPECT_FLOAT_EQ(1.0, ts.getValue(5.0)); + EXPECT_FLOAT_EQ(2.0, ts.getValue(6.0)); + EXPECT_FLOAT_EQ(7.0, ts.getValue(10.0)); + EXPECT_FLOAT_EQ(7.0, ts.getValue(20.0)); + + ts.appendValue(11.0, -7.0); + EXPECT_FLOAT_EQ(-7.0, ts.getMin()); + EXPECT_FLOAT_EQ(10.0, ts.getMax()); +} + +TEST_F(TimeSeriesTests, rangeOperations) { + TimeSeries ts(5); + ts.appendValue(0.0, 0.0); + ts.appendValue(1.0, 1.0); + ts.appendValue(2.0, 2.0); + + EXPECT_FLOAT_EQ(0.0, ts.getMin(-1.0, 100.0)); + EXPECT_FLOAT_EQ(2.0, ts.getMax(-1.0, 100.0)); + + EXPECT_FLOAT_EQ(0.0, ts.getMin(-1.0, 1.5)); + EXPECT_FLOAT_EQ(1.0, ts.getMax(-1.0, 1.5)); + + EXPECT_FLOAT_EQ(1.0, ts.getMin(0.5, 1.5)); + EXPECT_FLOAT_EQ(1.0, ts.getMax(-1.0, 1.5)); + EXPECT_FLOAT_EQ(1.0, ts.getMax(1.0, 1.5)); + + EXPECT_FLOAT_EQ(1.5, ts.getAverage(0.5, 2.0)); + EXPECT_FLOAT_EQ(1.5, ts.getAverage(0.5, 3.0)); + + ts.appendValue(3.0, 3.0); + ts.appendValue(4.0, 4.0); + ts.appendValue(5.0, 5.0); + ts.appendValue(6.0, 6.0); + + // Current circular buffer layout should be: 5 6 | 2 3 4 + // Cover corner cases by sampling different ranges + EXPECT_EQ(1, ts.getCount(6.0, 6.0)); + EXPECT_EQ(2, ts.getCount(4.5, 6.0)); + EXPECT_EQ(5, ts.getCount(0.0, 6.0)); + EXPECT_EQ(5, ts.getCount(0.0, 7.0)); + EXPECT_EQ(2, ts.getCount(3.5, 5.0)); + EXPECT_EQ(0, ts.getCount(7.0, 8.0)); + EXPECT_EQ(2, ts.getCount(4.5, 7.0)); + EXPECT_EQ(2, ts.getCount(3.5, 5.5)); + + EXPECT_FLOAT_EQ(2.0, ts.getMin(0.0, 2.0)); + EXPECT_FLOAT_EQ(2.0, ts.getMin(0.0, 2.5)); + EXPECT_FLOAT_EQ(0.0, ts.getMin(0.0, 1.0)); + EXPECT_FLOAT_EQ(0.0, ts.getMin(100.0, 101.0)); + EXPECT_FLOAT_EQ(0.0, ts.getAverage(0.0, 1.0)); + + EXPECT_FLOAT_EQ(4.0, ts.getMin(3.5, 5.5)); + EXPECT_FLOAT_EQ(5.0, ts.getMax(3.5, 5.5)); + EXPECT_FLOAT_EQ(4.5, ts.getAverage(3.5, 5.5)); + + EXPECT_FLOAT_EQ(3.0, ts.getMin(2.5, 4.5)); + EXPECT_FLOAT_EQ(4.0, ts.getMax(2.5, 4.5)); + EXPECT_FLOAT_EQ(3.5, ts.getAverage(2.5, 4.5)); + + EXPECT_FLOAT_EQ(6.0, ts.getMin(5.5, 7.5)); + EXPECT_FLOAT_EQ(6.0, ts.getMax(5.5, 7.5)); + EXPECT_FLOAT_EQ(6.0, ts.getAverage(5.5, 7.5)); + + EXPECT_FLOAT_EQ(5.0, ts.getMin(4.5, 7.5)); + EXPECT_FLOAT_EQ(6.0, ts.getMax(4.5, 7.5)); + EXPECT_FLOAT_EQ(5.5, ts.getAverage(4.5, 7.5)); +} + +TEST_F(TimeSeriesTests, accumulateOperations) { + TimeSeries ts(5); + ts.accumulateValue(1.0, 2.0, 1.0); + + EXPECT_EQ(2, ts.getNumPoints()); + + EXPECT_FLOAT_EQ(1.0, ts.timeAtIndex(0)); + EXPECT_FLOAT_EQ(2.0, ts.timeAtIndex(1)); + + EXPECT_FLOAT_EQ(0.0, ts.valueAtIndex(0)); + EXPECT_FLOAT_EQ(1.0, ts.valueAtIndex(1)); + + ts.appendValue(3.0, 2.0); + + ts.accumulateValue(1.5, 4.5, 3.0); + EXPECT_EQ(4, ts.getNumPoints()); + + EXPECT_FLOAT_EQ(0.0, ts.valueAtIndex(0)); + EXPECT_FLOAT_EQ(1.5, ts.valueAtIndex(1)); + EXPECT_FLOAT_EQ(3.0, ts.valueAtIndex(2)); + EXPECT_FLOAT_EQ(1.5, ts.valueAtIndex(3)); + + EXPECT_FLOAT_EQ(1.0, ts.timeAtIndex(0)); + EXPECT_FLOAT_EQ(2.0, ts.timeAtIndex(1)); + EXPECT_FLOAT_EQ(3.0, ts.timeAtIndex(2)); + EXPECT_FLOAT_EQ(4.5, ts.timeAtIndex(3)); + + ts.accumulateValue(2.0, 3.5, 1.5); + + EXPECT_EQ(4, ts.getNumPoints()); + + EXPECT_FLOAT_EQ(0.0, ts.valueAtIndex(0)); + EXPECT_FLOAT_EQ(1.5, ts.valueAtIndex(1)); + EXPECT_FLOAT_EQ(4.0, ts.valueAtIndex(2)); + EXPECT_FLOAT_EQ(2.0, ts.valueAtIndex(3)); + + EXPECT_FLOAT_EQ(1.0, ts.timeAtIndex(0)); + EXPECT_FLOAT_EQ(2.0, ts.timeAtIndex(1)); + EXPECT_FLOAT_EQ(3.0, ts.timeAtIndex(2)); + EXPECT_FLOAT_EQ(4.5, ts.timeAtIndex(3)); + + ts.appendValue(5.0, 0.0); + ts.appendValue(5.5, 0.0); + ts.appendValue(6.0, 0.0); + + EXPECT_EQ(5, ts.getNumPoints()); + + ts.accumulateValue(4.0, 7.0, 6.0); + EXPECT_EQ(5, ts.getNumPoints()); + + EXPECT_EQ(3, ts.findHistoryPointIndex(4.0, TimeSeries::Bound::Upper)); + + EXPECT_FLOAT_EQ(3.0, ts.valueAtIndex(0)); + EXPECT_FLOAT_EQ(1.0, ts.valueAtIndex(1)); + EXPECT_FLOAT_EQ(1.0, ts.valueAtIndex(2)); + EXPECT_FLOAT_EQ(1.0, ts.valueAtIndex(3)); + EXPECT_FLOAT_EQ(2.0, ts.valueAtIndex(4)); + + EXPECT_FLOAT_EQ(4.5, ts.timeAtIndex(0)); + EXPECT_FLOAT_EQ(5.0, ts.timeAtIndex(1)); + EXPECT_FLOAT_EQ(5.5, ts.timeAtIndex(2)); + EXPECT_FLOAT_EQ(6.0, ts.timeAtIndex(3)); + EXPECT_FLOAT_EQ(7.0, ts.timeAtIndex(4)); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/tracy_noop.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/tracy_noop.h new file mode 100644 index 0000000000..40ec4d9e99 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/profiling/tracy_noop.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#ifndef RNCXX_WITH_PROFILING_PROVIDER +#define RNCXX_WITH_PROFILING_PROVIDER 0 +#endif + +#if RNCXX_WITH_PROFILING_PROVIDER +#include + +#ifndef TRACE_FUNCTION +#if defined(__GNUC__) || defined(__clang__) +#define TRACE_FUNCTION __PRETTY_FUNCTION__ +#else +#define TRACE_FUNCTION __FUNCTION__ +#endif +#endif // TRACE_FUNCTION + +static inline constexpr char kTraceCategory[] = "react_native"; + +HZT_DEFINE_TRACING_CATEGORIES( + facebook::react, + horizon::tracing::Category(kTraceCategory, "react_native", horizon::tracing::StrippingLevel::Important)); + +#define SCOPED_TRACE_CPU_AUTO() HZT_TRACE_SCOPE_NS(::facebook::react, kTraceCategory, TRACE_FUNCTION); + +#define SCOPED_TRACE_CPU(name) HZT_TRACE_SCOPE_NS(::facebook::react, kTraceCategory, name); + +#else +#ifndef SCOPED_TRACE_CPU_AUTO +#define SCOPED_TRACE_CPU_AUTO() ((void)0) +#endif +#ifndef SCOPED_TRACE_CPU +#define SCOPED_TRACE_CPU(name) ((void)0) +#endif +#endif diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.cpp new file mode 100644 index 0000000000..a48e6c631d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.cpp @@ -0,0 +1,83 @@ +#include "DesktopContextMenuView.h" + +#include +#include +#include +#include + +namespace facebook::react { + +const char DesktopContextMenuViewComponentName[] = "DesktopContextMenuView"; + +DesktopContextMenuViewProps::DesktopContextMenuViewProps( + const PropsParserContext& context, + const DesktopContextMenuViewProps& sourceProps, + const RawProps& rawProps) + : ViewProps(context, sourceProps, rawProps), + // `convertRawProp` reaches `folly::dynamic` through `RawValue`'s explicit + // conversion, so the recursive model arrives as-is: no serialisation in + // JS and no second shape to keep in step with `MenuItemSpec`. + items(convertRawProp( + context, + rawProps, + "items", + sourceProps.items, + folly::dynamic::array())), + presentation(convertRawProp( + context, + rawProps, + "presentation", + sourceProps.presentation, + std::string{"default"})), + disabled(convertRawProp( + context, + rawProps, + "disabled", + sourceProps.disabled, + false)) {} + +#ifdef RN_SERIALIZABLE_STATE +ComponentName DesktopContextMenuViewProps::getDiffPropsImplementationTarget() + const { + return DesktopContextMenuViewComponentName; +} + +folly::dynamic DesktopContextMenuViewProps::getDiffProps( + const Props* prevProps) const { + static const auto defaultProps = DesktopContextMenuViewProps(); + const auto* oldProps = prevProps == nullptr + ? &defaultProps + : static_cast(prevProps); + if (this == oldProps) { + return folly::dynamic::object(); + } + + folly::dynamic result = ViewProps::getDiffProps(prevProps); + // Compared as values, so a rebuilt array with the same entries is not a + // change. That is what makes a memoised-but-reallocated `items` cheap. + if (!(items == oldProps->items)) { + result["items"] = items; + } + if (presentation != oldProps->presentation) { + result["presentation"] = presentation; + } + if (disabled != oldProps->disabled) { + result["disabled"] = disabled; + } + return result; +} +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE +SharedDebugStringConvertibleList DesktopContextMenuViewProps::getDebugProps() + const { + return ViewProps::getDebugProps() + + SharedDebugStringConvertibleList{ + debugStringConvertibleItem("items", folly::toJson(items)), + debugStringConvertibleItem("presentation", presentation), + debugStringConvertibleItem("disabled", disabled), + }; +} +#endif + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.h new file mode 100644 index 0000000000..5718567a0b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +// Hand-written rather than generated: core has no component codegen, because +// autolink only runs it for dependencies and core is the runtime. This follows +// `windowcontrol/WindowControl.h`, core's other hand-registered component. +namespace facebook::react { + +extern const char DesktopContextMenuViewComponentName[]; + +/// A view whose secondary click opens a menu from native. +/// +/// `items` is the menu model itself — a recursive array that codegen cannot +/// describe, which is why the JS spec types it as `UnsafeMixed` and it arrives +/// here as `folly::dynamic`. The Rust renderer reads it as JSON from the props +/// reader, in the same shape `MenuItemSpec` uses everywhere else, so a menu +/// shown by this component and one shown by `ContextMenu.show` cannot differ. +class DesktopContextMenuViewProps final : public ViewProps { + public: + DesktopContextMenuViewProps() = default; + DesktopContextMenuViewProps( + const PropsParserContext& context, + const DesktopContextMenuViewProps& sourceProps, + const RawProps& rawProps); + + // Copy-init, not brace-init: `folly::dynamic` has an initializer-list + // constructor, so braces here would build a one-element array holding an empty + // array. + folly::dynamic items = folly::dynamic::array(); + /// `default`, `native` or `custom`. `custom` is opened by JS, so the native + /// renderer leaves it alone; `default` means whatever the app asked for, + /// which this path can only serve as a native menu. + std::string presentation{"default"}; + bool disabled{false}; + +#ifdef RN_SERIALIZABLE_STATE + ComponentName getDiffPropsImplementationTarget() const override; + folly::dynamic getDiffProps(const Props* prevProps) const override; +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE + SharedDebugStringConvertibleList getDebugProps() const override; +#endif +}; + +using DesktopContextMenuViewShadowNode = ConcreteViewShadowNode< + DesktopContextMenuViewComponentName, + DesktopContextMenuViewProps, + ViewEventEmitter>; +using DesktopContextMenuViewComponentDescriptor = + ConcreteComponentDescriptor; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/nativeviewhost/NativeViewHost.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/nativeviewhost/NativeViewHost.cpp new file mode 100644 index 0000000000..90f54c593b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/nativeviewhost/NativeViewHost.cpp @@ -0,0 +1,60 @@ +#include "NativeViewHost.h" + +#include +#include +#include +#include + +namespace facebook::react { + +const char NativeViewHostComponentName[] = "NativeViewHost"; + +NativeViewHostProps::NativeViewHostProps( + const PropsParserContext& context, + const NativeViewHostProps& sourceProps, + const RawProps& rawProps) + : ViewProps(context, sourceProps, rawProps), + viewName(convertRawProp( + context, + rawProps, + "viewName", + sourceProps.viewName, + {std::string{}})), + nativePropsJson(convertRawProp( + context, + rawProps, + "nativePropsJson", + sourceProps.nativePropsJson, + {std::string{"{}"}})) {} + +#ifdef RN_SERIALIZABLE_STATE +ComponentName NativeViewHostProps::getDiffPropsImplementationTarget() const { + return "NativeViewHost"; +} + +folly::dynamic NativeViewHostProps::getDiffProps(const Props* prevProps) const { + static const auto defaultProps = NativeViewHostProps(); + const auto* oldProps = prevProps == nullptr + ? &defaultProps + : static_cast(prevProps); + if (this == oldProps) return folly::dynamic::object(); + + folly::dynamic result = ViewProps::getDiffProps(prevProps); + if (viewName != oldProps->viewName) result["viewName"] = viewName; + if (nativePropsJson != oldProps->nativePropsJson) { + result["nativePropsJson"] = nativePropsJson; + } + return result; +} +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE +SharedDebugStringConvertibleList NativeViewHostProps::getDebugProps() const { + return ViewProps::getDebugProps() + SharedDebugStringConvertibleList{ + debugStringConvertibleItem("viewName", viewName), + debugStringConvertibleItem("nativePropsJson", nativePropsJson), + }; +} +#endif + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/nativeviewhost/NativeViewHost.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/nativeviewhost/NativeViewHost.h new file mode 100644 index 0000000000..9b549d6823 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/nativeviewhost/NativeViewHost.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +extern const char NativeViewHostComponentName[]; + +class NativeViewHostProps final : public ViewProps { + public: + NativeViewHostProps() = default; + NativeViewHostProps( + const PropsParserContext& context, + const NativeViewHostProps& sourceProps, + const RawProps& rawProps); + + std::string viewName; + std::string nativePropsJson{"{}"}; + +#ifdef RN_SERIALIZABLE_STATE + ComponentName getDiffPropsImplementationTarget() const override; + folly::dynamic getDiffProps(const Props* prevProps) const override; +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE + SharedDebugStringConvertibleList getDebugProps() const override; +#endif +}; + +using NativeViewHostShadowNode = ConcreteViewShadowNode< + NativeViewHostComponentName, + NativeViewHostProps, + ViewEventEmitter>; +using NativeViewHostComponentDescriptor = + ConcreteComponentDescriptor; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/shortcutscope/ShortcutScope.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/shortcutscope/ShortcutScope.cpp new file mode 100644 index 0000000000..51c54051cc --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/shortcutscope/ShortcutScope.cpp @@ -0,0 +1,56 @@ +#include "ShortcutScope.h" + +#include +#include +#include +#include + +namespace facebook::react { + +const char ShortcutScopeComponentName[] = "DesktopShortcutScope"; + +ShortcutScopeProps::ShortcutScopeProps( + const PropsParserContext& context, + const ShortcutScopeProps& sourceProps, + const RawProps& rawProps) + : ViewProps(context, sourceProps, rawProps), + name(convertRawProp( + context, + rawProps, + "name", + sourceProps.name, + std::string{})) {} + +#ifdef RN_SERIALIZABLE_STATE +ComponentName ShortcutScopeProps::getDiffPropsImplementationTarget() const { + return ShortcutScopeComponentName; +} + +folly::dynamic ShortcutScopeProps::getDiffProps(const Props* prevProps) const { + static const auto defaultProps = ShortcutScopeProps(); + const auto* oldProps = prevProps == nullptr + ? &defaultProps + : static_cast(prevProps); + if (this == oldProps) { + return folly::dynamic::object(); + } + + folly::dynamic result = ViewProps::getDiffProps(prevProps); + // A renamed scope is a different scope: every shortcut registered with + // `when: ` stops matching, so the rename has to reach the renderer. + if (name != oldProps->name) { + result["name"] = name; + } + return result; +} +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE +SharedDebugStringConvertibleList ShortcutScopeProps::getDebugProps() const { + return ViewProps::getDebugProps() + SharedDebugStringConvertibleList{ + debugStringConvertibleItem("name", name), + }; +} +#endif + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/shortcutscope/ShortcutScope.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/shortcutscope/ShortcutScope.h new file mode 100644 index 0000000000..004c505694 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/shortcutscope/ShortcutScope.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +// Hand-written rather than generated: core has no component codegen, because +// autolink only runs it for dependencies and core is the runtime. This follows +// `windowcontrol/WindowControl.h` and +// `desktopcontextmenuview/DesktopContextMenuView.h`, core's other +// hand-registered components. +// +// The component is a plain box with a name. Everything it means is decided +// natively: the Rust renderer turns `name` into a keymap context, and GPUI's +// keymap does the precedence. So the props here are one string, and the rest of +// the behaviour lives in `rn-gpui/src/builtins/shortcut_scope`. +namespace facebook::react { + +extern const char ShortcutScopeComponentName[]; + +/// A named scope for keyboard shortcuts. Scopes nest; an inner scope's binding +/// outranks an outer one's. +class ShortcutScopeProps final : public ViewProps { + public: + ShortcutScopeProps() = default; + ShortcutScopeProps( + const PropsParserContext& context, + const ShortcutScopeProps& sourceProps, + const RawProps& rawProps); + + /// The name a `useShortcut`'s `when` refers to. Empty claims no context. + std::string name{}; + +#ifdef RN_SERIALIZABLE_STATE + ComponentName getDiffPropsImplementationTarget() const override; + folly::dynamic getDiffProps(const Props* prevProps) const override; +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE + SharedDebugStringConvertibleList getDebugProps() const override; +#endif +}; + +using ShortcutScopeShadowNode = ConcreteViewShadowNode< + ShortcutScopeComponentName, + ShortcutScopeProps, + ViewEventEmitter>; +using ShortcutScopeComponentDescriptor = + ConcreteComponentDescriptor; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/windowcontrol/WindowControl.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/windowcontrol/WindowControl.cpp new file mode 100644 index 0000000000..cdd029253e --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/windowcontrol/WindowControl.cpp @@ -0,0 +1,75 @@ +#include "WindowControl.h" + +#include +#include +#include +#include + +namespace facebook::react { + +const char WindowControlComponentName[] = "DesktopWindowControl"; + +namespace { + +WindowControlAction parseAction( + const PropsParserContext& context, + const RawProps& rawProps, + const std::string& fallback) { + const std::string action = convertRawProp( + context, rawProps, "action", fallback, std::string{"close"}); + if (action == "minimize") return WindowControlAction::Minimize; + if (action == "maximize") return WindowControlAction::Maximize; + return WindowControlAction::Close; +} + +const char* actionName(WindowControlAction action) { + switch (action) { + case WindowControlAction::Minimize: + return "minimize"; + case WindowControlAction::Maximize: + return "maximize"; + default: + return "close"; + } +} + +} // namespace + +WindowControlProps::WindowControlProps( + const PropsParserContext& context, + const WindowControlProps& sourceProps, + const RawProps& rawProps) + : ViewProps(context, sourceProps, rawProps), + action(parseAction(context, rawProps, actionName(sourceProps.action))), + disabled(convertRawProp( + context, rawProps, "disabled", sourceProps.disabled, false)) {} + +#ifdef RN_SERIALIZABLE_STATE +ComponentName WindowControlProps::getDiffPropsImplementationTarget() const { + return "DesktopWindowControl"; +} + +folly::dynamic WindowControlProps::getDiffProps(const Props* prevProps) const { + static const auto defaultProps = WindowControlProps(); + const auto* oldProps = prevProps == nullptr + ? &defaultProps + : static_cast(prevProps); + if (this == oldProps) return folly::dynamic::object(); + + folly::dynamic result = ViewProps::getDiffProps(prevProps); + if (action != oldProps->action) result["action"] = actionName(action); + if (disabled != oldProps->disabled) result["disabled"] = disabled; + return result; +} +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE +SharedDebugStringConvertibleList WindowControlProps::getDebugProps() const { + return ViewProps::getDebugProps() + SharedDebugStringConvertibleList{ + debugStringConvertibleItem("action", std::string(actionName(action))), + debugStringConvertibleItem("disabled", disabled), + }; +} +#endif + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/windowcontrol/WindowControl.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/windowcontrol/WindowControl.h new file mode 100644 index 0000000000..c3d83099d9 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/components/windowcontrol/WindowControl.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Hand-written rather than generated: core has no component codegen, because +// autolink only runs it for dependencies and core is the runtime. This follows +// `nativeviewhost/NativeViewHost.h`, core's other hand-registered component. +namespace facebook::react { + +extern const char WindowControlComponentName[]; + +/// The action a caption button performs. +enum class WindowControlAction { + Minimize, + Maximize, + Close, +}; + +class WindowControlProps final : public ViewProps { + public: + WindowControlProps() = default; + WindowControlProps( + const PropsParserContext& context, + const WindowControlProps& sourceProps, + const RawProps& rawProps); + + WindowControlAction action{WindowControlAction::Close}; + bool disabled{false}; + +#ifdef RN_SERIALIZABLE_STATE + ComponentName getDiffPropsImplementationTarget() const override; + folly::dynamic getDiffProps(const Props* prevProps) const override; +#endif + +#if RN_DEBUG_STRING_CONVERTIBLE + SharedDebugStringConvertibleList getDebugProps() const override; +#endif +}; + +using WindowControlShadowNode = ConcreteViewShadowNode< + WindowControlComponentName, + WindowControlProps, + ViewEventEmitter>; +using WindowControlComponentDescriptor = + ConcreteComponentDescriptor; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/CMakeLists.txt new file mode 100644 index 0000000000..0f47b39da8 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_renderer_scheduler_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_cxx_platform_react_renderer_scheduler OBJECT ${react_cxx_platform_react_renderer_scheduler_SRC}) + +target_include_directories(react_cxx_platform_react_renderer_scheduler PUBLIC ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_renderer_scheduler + react_renderer_mounting + react_renderer_scheduler + react_cxx_platform_react_renderer_uimanager +) +target_compile_reactnative_options(react_cxx_platform_react_renderer_scheduler PRIVATE) +target_compile_options(react_cxx_platform_react_renderer_scheduler PRIVATE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp new file mode 100644 index 0000000000..ebf5669b87 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "SchedulerDelegateImpl.h" +#include + +namespace facebook::react { + +SchedulerDelegateImpl::SchedulerDelegateImpl( + std::shared_ptr mountingManager) noexcept + : mountingManager_(std::move(mountingManager)) {} + +void SchedulerDelegateImpl::setUIManager( + std::shared_ptr uiManager) noexcept { + uiManager_ = uiManager; +} + +void SchedulerDelegateImpl::schedulerDidFinishTransaction( + const std::shared_ptr& /*mountingCoordinator*/) { + // no-op, we will flush the transaction from schedulerShouldRenderTransactions +} + +void SchedulerDelegateImpl::schedulerShouldRenderTransactions( + const std::shared_ptr& mountingCoordinator) { + auto surfaceId = mountingCoordinator->getSurfaceId(); + if (auto transaction = mountingCoordinator->pullTransaction(); + transaction.has_value()) { + if (auto& transactionValue = transaction.value(); + !transactionValue.getMutations().empty()) { + mountingManager_->executeMount(surfaceId, std::move(transactionValue)); + } + } +} + +void SchedulerDelegateImpl::schedulerShouldMergeReactRevision( + SurfaceId surfaceId) { + uiManager_->getShadowTreeRegistry().visit( + surfaceId, + [](const ShadowTree& shadowTree) { shadowTree.mergeReactRevision(); }); +} + +void SchedulerDelegateImpl::schedulerDidRequestPreliminaryViewAllocation( + const ShadowNode& shadowNode) {} + +void SchedulerDelegateImpl::schedulerDidDispatchCommand( + const ShadowView& shadowView, + const std::string& commandName, + const folly::dynamic& args) { + mountingManager_->dispatchCommand(shadowView, commandName, args); +} + +void SchedulerDelegateImpl::schedulerDidSetIsJSResponder( + const ShadowView& shadowView, + bool isJSResponder, + bool blockNativeResponder) { + mountingManager_->setIsJSResponder( + shadowView, isJSResponder, blockNativeResponder); +} + +void SchedulerDelegateImpl::schedulerDidSendAccessibilityEvent( + const ShadowView& shadowView, + const std::string& eventType) { + if (eventType == "focus") { + mountingManager_->setAccessibilityFocusedView(shadowView.tag); + } else if (eventType == "click") { + mountingManager_->accessibleClickAction(shadowView.tag); + } +} + +void SchedulerDelegateImpl::schedulerShouldSynchronouslyUpdateViewOnUIThread( + Tag tag, + const folly::dynamic& props) { + mountingManager_->synchronouslyUpdateViewOnUIThread(tag, props); +} + +void SchedulerDelegateImpl::schedulerDidUpdateShadowTree( + const std::unordered_map& tagToProps) { + mountingManager_->onUpdateShadowTree(tagToProps); +} + +void SchedulerDelegateImpl::schedulerDidCaptureViewSnapshot( + Tag /*tag*/, + SurfaceId /*surfaceId*/) {} + +void SchedulerDelegateImpl::schedulerDidSetViewSnapshot( + Tag /*sourceTag*/, + Tag /*targetTag*/, + SurfaceId /*surfaceId*/) {} + +void SchedulerDelegateImpl::schedulerDidClearPendingSnapshots() {} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h new file mode 100644 index 0000000000..089b64d573 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +class IMountingManager; + +class SchedulerDelegateImpl : public SchedulerDelegate { + public: + SchedulerDelegateImpl(std::shared_ptr mountingManager) noexcept; + + ~SchedulerDelegateImpl() noexcept override = default; + + SchedulerDelegateImpl(SchedulerDelegateImpl &&) noexcept = default; + SchedulerDelegateImpl &operator=(SchedulerDelegateImpl &&) noexcept = default; + SchedulerDelegateImpl(const SchedulerDelegateImpl &) = delete; + SchedulerDelegateImpl &operator=(const SchedulerDelegateImpl &) = delete; + + void setUIManager(std::shared_ptr uiManager) noexcept; + + private: + void schedulerDidFinishTransaction(const std::shared_ptr &mountingCoordinator) override; + + void schedulerShouldRenderTransactions( + const std::shared_ptr &mountingCoordinator) override; + + void schedulerShouldMergeReactRevision(SurfaceId surfaceId) override; + + void schedulerDidRequestPreliminaryViewAllocation(const ShadowNode &shadowNode) override; + + void schedulerDidDispatchCommand( + const ShadowView &shadowView, + const std::string &commandName, + const folly::dynamic &args) override; + + void schedulerDidSetIsJSResponder(const ShadowView &shadowView, bool isJSResponder, bool blockNativeResponder) + override; + + void schedulerDidSendAccessibilityEvent(const ShadowView &shadowView, const std::string &eventType) override; + + void schedulerShouldSynchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic &props) override; + + void schedulerDidUpdateShadowTree(const std::unordered_map &tagToProps) override; + + void schedulerDidCaptureViewSnapshot(Tag tag, SurfaceId surfaceId) override; + + void schedulerDidSetViewSnapshot(Tag sourceTag, Tag targetTag, SurfaceId surfaceId) override; + + void schedulerDidClearPendingSnapshots() override; + + std::shared_ptr mountingManager_; + std::shared_ptr uiManager_; +}; + +}; // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SurfaceDelegate.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SurfaceDelegate.h new file mode 100644 index 0000000000..8797e67948 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/scheduler/SurfaceDelegate.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react { +/** + * Interface for handling a surface in React Native. Each platform can have + * custom logic to show/hide. + * Inspired by + * xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/SurfaceDelegate.java + */ +class SurfaceDelegate { + public: + virtual ~SurfaceDelegate() = default; + + /** + * Create the React content view that uses the appKey as the React application + * name + */ + virtual void createContentView(std::string appKey) = 0; + + /** + * Check if the content view is created and ready to be shown + */ + virtual bool isContentViewReady() = 0; + + /** Destroy the React content view to avoid memory leak */ + virtual void destroyContentView() = 0; + + /** Show the surface containing the React content view */ + virtual void show() = 0; + + /** Hide the surface containing the React content view */ + virtual void hide() = 0; + + /** Check if the surface is currently showing */ + virtual bool isShowing() = 0; +}; +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/uimanager/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/uimanager/CMakeLists.txt new file mode 100644 index 0000000000..929954e700 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/uimanager/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +add_library(react_cxx_platform_react_renderer_uimanager INTERFACE) + +target_include_directories(react_cxx_platform_react_renderer_uimanager INTERFACE ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_renderer_uimanager INTERFACE + react_renderer_core + react_renderer_componentregistry + react_renderer_mounting +) +target_compile_reactnative_options(react_cxx_platform_react_renderer_uimanager INTERFACE) +target_compile_options(react_cxx_platform_react_renderer_uimanager INTERFACE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/uimanager/IMountingManager.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/uimanager/IMountingManager.h new file mode 100644 index 0000000000..b68da46f54 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/renderer/uimanager/IMountingManager.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class Scheduler; +class UIManager; +class IImageLoader; + +using SchedulerTask = std::function; +using SchedulerTaskExecutor = std::function; + +using EventEmitterListener = + std::function; + +class IMountingManager { + public: + IMountingManager() noexcept = default; + + virtual ~IMountingManager() noexcept = default; + + IMountingManager(IMountingManager &&) noexcept = default; + IMountingManager &operator=(IMountingManager &&) noexcept = default; + IMountingManager(const IMountingManager &) = delete; + IMountingManager &operator=(const IMountingManager &) = delete; + + virtual void executeMount(SurfaceId surfaceId, MountingTransaction &&mountingTransaction) = 0; + + virtual void + dispatchCommand(const ShadowView &shadowView, const std::string &commandName, const folly::dynamic &args) = 0; + + virtual void + setIsJSResponder(const ShadowView & /* shadowView */, bool /* isJSResponder */, bool /* blockNativeResponder */) + { + } + + virtual void synchronouslyUpdateViewOnUIThread(Tag /* reactTag */, const folly::dynamic & /* changedProps */) {} + + virtual void onUpdateShadowTree(const std::unordered_map & /* tagToProps */) {} + + virtual void initializeAccessibilityManager() {} + + virtual void setAccessibilityFocusedView(Tag /* viewTag */) {} + + virtual void setFocusedView(Tag /* viewTag */) {} + + virtual void clearAccessibilityFocusedView(Tag /* viewTag */) {} + + virtual void accessibleClickAction(Tag /* viewTag */) {} + + virtual void accessibleScrollInDirection(Tag /* viewTag */, int /* direction */) {} + + virtual void accessibleSetText(Tag /* viewTag */, const std::string & /* text */) {} + + virtual void clearFocusedView(Tag /* viewTag */) {} + + virtual void setAfterMountCallback(std::function && /* onAfterMount */) {} + + virtual ComponentRegistryFactory getComponentRegistryFactory() + { + return nullptr; + } + + virtual bool hasComponent(const std::string & /*name*/) + { + return false; + } + + virtual void setSchedulerTaskExecutor(SchedulerTaskExecutor && /* schedulerTaskExecutor */) noexcept {} + + virtual void setEventEmitterListener(std::shared_ptr /* listener */) noexcept {} + + virtual void setUIManager(std::weak_ptr /* uiManager */) noexcept {} + + virtual std::shared_ptr getImageLoader() noexcept + { + return nullptr; + } +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/CMakeLists.txt new file mode 100644 index 0000000000..ad9d5f94ff --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/CMakeLists.txt @@ -0,0 +1,51 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_runtime_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_cxx_platform_react_runtime OBJECT ${react_cxx_platform_react_runtime_SRC}) + +target_include_directories(react_cxx_platform_react_runtime + PUBLIC + ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_runtime + bridgeless + callinvoker + glog + jserrorhandler + jsinspector + react_codegen_rncore + react_cxx_platform_react_coremodules + react_cxx_platform_react_devsupport + react_cxx_platform_react_devsupport + react_cxx_platform_react_http + react_cxx_platform_react_io + react_cxx_platform_react_logging + react_cxx_platform_react_nativemodule + react_renderer_animated + react_renderer_animationbackend + react_cxx_platform_react_renderer_uimanager + react_cxx_platform_react_threading + react_cxx_platform_react_utils + react_cxxreact + react_debug + react_nativemodule_core + react_nativemodule_defaults + react_nativemodule_intersectionobserver + react_nativemodule_mutationobserver + react_nativemodule_webperformance + react_renderer_graphics + react_renderer_runtimescheduler + react_renderer_scheduler + rrc_native + rrc_view +) +target_compile_reactnative_options(react_cxx_platform_react_runtime PRIVATE) +target_compile_options(react_cxx_platform_react_runtime PRIVATE -Wpedantic -Wno-deprecated-declarations) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/GpuiDebug.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/GpuiDebug.h new file mode 100644 index 0000000000..8eaa9fcc60 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/GpuiDebug.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace facebook::react { + +inline bool gpuiDebugAnimationEnabled() noexcept { + static const bool enabled = std::getenv("RN_GPUI_DEBUG_ANIMATION") != nullptr; + return enabled; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/PlatformTimerRegistryImpl.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/PlatformTimerRegistryImpl.cpp new file mode 100644 index 0000000000..415236dadd --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/PlatformTimerRegistryImpl.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "PlatformTimerRegistryImpl.h" + +#include +#include +#include + +namespace facebook::react { + +std::mutex PlatformTimerRegistryImpl::registriesMutex_; +std::unordered_set PlatformTimerRegistryImpl::registries_; + +PlatformTimerRegistryImpl::PlatformTimerRegistryImpl() noexcept { + std::lock_guard guard(registriesMutex_); + registries_.insert(this); +} + +PlatformTimerRegistryImpl::~PlatformTimerRegistryImpl() noexcept { + quit(); + std::lock_guard guard(registriesMutex_); + registries_.erase(this); +} + +uint64_t PlatformTimerRegistryImpl::pendingDueWithin(uint64_t windowMs) noexcept { + const auto limit = std::chrono::steady_clock::now() + + std::chrono::milliseconds(windowMs); + std::lock_guard registriesGuard( + PlatformTimerRegistryImpl::registriesMutex_); + uint64_t count = 0; + for (auto* registry : PlatformTimerRegistryImpl::registries_) { + std::lock_guard timersGuard(registry->timersMutex_); + for (const auto& [id, timer] : registry->timers_) { + (void)id; + if (!timer.isRecurring && timer.due <= limit) ++count; + } + } + return count; +} + +extern "C" uint64_t rn_gpui_pending_timers_due_within( + uint64_t windowMs) noexcept { + return PlatformTimerRegistryImpl::pendingDueWithin(windowMs); +} + +void PlatformTimerRegistryImpl::quit() { + LOG(INFO) << "Shutting down PlatformTimerRegistryImpl..."; + taskDispatchThread_.quit(); + std::lock_guard guard(timersMutex_); + timers_.clear(); +} + +void PlatformTimerRegistryImpl::createTimer(uint32_t timerId, double delayMs) { + createTimerInternal(timerId, delayMs, false); +} + +void PlatformTimerRegistryImpl::deleteTimer(uint32_t timerId) { + std::lock_guard guard(timersMutex_); + if (auto it = timers_.find(timerId); it != timers_.end()) { + timers_.erase(it); + } +} + +void PlatformTimerRegistryImpl::createRecurringTimer( + uint32_t timerId, + double delayMs) { + createTimerInternal(timerId, delayMs, true); +} + +void PlatformTimerRegistryImpl::createTimerInternal( + uint32_t timerId, + double delayMs, + bool recurring) { + { + std::lock_guard guard(timersMutex_); + timers_.emplace( + timerId, + Timer{ + .timerId = timerId, + .durationMs = delayMs, + .isRecurring = recurring}); + } + startTimer(timerId, delayMs); +} + +void PlatformTimerRegistryImpl::startTimer(uint32_t timerId, double delayMs) { + // RN's generic TimerManager currently implements requestAnimationFrame as + // setTimeout(0). Re-arming that timer with a zero delay spins this worker and + // floods the JS runtime (observed at >70k Animated.Value updates/second), + // starving input and making transforms visibly janky. Pace sub-frame timers + // at 60 Hz; JS still receives the normal timer callback, but at a useful + // desktop frame cadence instead of an unbounded tight loop. + constexpr double kMinimumFrameTimerMs = 1000.0 / 60.0; + const auto scheduledDelayMs = std::max(delayMs, kMinimumFrameTimerMs); + { + std::lock_guard guard(timersMutex_); + if (auto it = timers_.find(timerId); it != timers_.end()) { + it->second.due = std::chrono::steady_clock::now() + + std::chrono::milliseconds(static_cast(scheduledDelayMs)); + } else { + return; + } + } + taskDispatchThread_.runAsync( + [this, timerId, delayMs]() { + bool isRecurring = true; + { + std::lock_guard guard(timersMutex_); + auto it = timers_.find(timerId); + if (it == timers_.end()) { + // timer was deleted before it could have been dispatched (at this + // iteration) + return; + } + if (!it->second.isRecurring) { + timers_.erase(it); + isRecurring = false; + } + } + + if (auto timerManagerStrong = timerManager_.lock()) { + timerManagerStrong->callTimer(timerId); + } + + if (isRecurring) { + startTimer(timerId, delayMs); + } + }, + std::chrono::milliseconds(static_cast(scheduledDelayMs))); +} + +void PlatformTimerRegistryImpl::setTimerManager( + std::weak_ptr timerManager) { + timerManager_ = timerManager; +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/PlatformTimerRegistryImpl.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/PlatformTimerRegistryImpl.h new file mode 100644 index 0000000000..19f027260b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/PlatformTimerRegistryImpl.h @@ -0,0 +1,63 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class PlatformTimerRegistryImpl : public PlatformTimerRegistry { + public: + PlatformTimerRegistryImpl() noexcept; + PlatformTimerRegistryImpl(const PlatformTimerRegistryImpl &) = delete; + PlatformTimerRegistryImpl(PlatformTimerRegistryImpl &&) = delete; + PlatformTimerRegistryImpl &operator=(const PlatformTimerRegistryImpl &) = delete; + PlatformTimerRegistryImpl &operator=(PlatformTimerRegistryImpl &&) = delete; + ~PlatformTimerRegistryImpl() noexcept override; + + void createTimer(uint32_t timerId, double delayMs) override; + + void deleteTimer(uint32_t timerId) override; + + void createRecurringTimer(uint32_t timerID, double delayMs) override; + + void setTimerManager(std::weak_ptr timerManager) override; + + void quit() override; + static uint64_t pendingDueWithin(uint64_t windowMs) noexcept; + + private: + struct Timer { + uint32_t timerId{0}; + double durationMs{0.0}; + bool isRecurring{false}; + std::chrono::steady_clock::time_point due; + }; + + TaskDispatchThread taskDispatchThread_{"PlatformTimerRegistry"}; + std::weak_ptr timerManager_; + std::unordered_map timers_; + std::mutex timersMutex_; + + void createTimerInternal(uint32_t timerID, double delayMs, bool isRecurring = false); + + void startTimer(uint32_t timerId, double delayMs); + static std::mutex registriesMutex_; + static std::unordered_set registries_; +}; + +extern "C" uint64_t rn_gpui_pending_timers_due_within(uint64_t windowMs) noexcept; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp new file mode 100644 index 0000000000..38899e5771 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ReactCxxTurboModuleProvider.h" +#include "GpuiDebug.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace facebook::react; + +ReactCxxTurboModuleProvider::ReactCxxTurboModuleProvider( + TurboModuleProviders turboModuleProviders, + std::shared_ptr jsInvoker, + JsErrorHandler::OnJsError onJsError, + std::shared_ptr + animatedNodesManagerProvider, + std::shared_ptr devServerHelper, + std::shared_ptr devUIDelegate, + std::shared_ptr logBoxSurfaceDelegate, + HttpClientFactory httpClientFactory, + WebSocketClientFactory webSocketClientFactory, + std::function liveReloadCallback, + std::shared_ptr sourceURL, + std::weak_ptr imageLoader) + : turboModuleProviders_(std::move(turboModuleProviders)), + jsInvoker_(std::move(jsInvoker)), + onJsError_(std::move(onJsError)), + animatedNodesManagerProvider_(std::move(animatedNodesManagerProvider)), + devServerHelper_(std::move(devServerHelper)), + devUIDelegate_(std::move(devUIDelegate)), + logBoxSurfaceDelegate_(std::move(logBoxSurfaceDelegate)), + httpClientFactory_(std::move(httpClientFactory)), + webSocketClientFactory_(std::move(webSocketClientFactory)), + liveReloadCallback_(std::move(liveReloadCallback)), + sourceURL_(std::move(sourceURL)), + imageLoader_(std::move(imageLoader)) {} + +std::shared_ptr ReactCxxTurboModuleProvider::operator()( + const std::string& name) const { + react_native_assert(!name.empty() && "TurboModule name must not be empty"); + + for (const auto& turboModuleProvider : turboModuleProviders_) { + if (turboModuleProvider) { + if (auto turboModule = turboModuleProvider(name, jsInvoker_)) { + return turboModule; + } + } + } + + if (animatedNodesManagerProvider_ != nullptr && + name == AnimatedModule::kModuleName) { + if (gpuiDebugAnimationEnabled()) { + LOG(INFO) << "[native-animated] NativeAnimatedModule requested"; + } + // when animatedNodesManagerProvider_ is null, defer to default + return std::make_shared( + jsInvoker_, animatedNodesManagerProvider_); + } else if (name == AppStateModule::kModuleName) { + return std::make_shared(jsInvoker_); + } else if (name == DeviceInfoModule::kModuleName) { + return std::make_shared(jsInvoker_); + } else if ( + devUIDelegate_ != nullptr && name == DevLoadingViewModule::kModuleName) { + return std::make_shared(jsInvoker_, devUIDelegate_); + } else if (devServerHelper_ && name == DevSettingsModule::kModuleName) { + return std::make_shared( + jsInvoker_, devServerHelper_, liveReloadCallback_); + } else if (name == PlatformConstantsModule::kModuleName) { + return std::make_shared(jsInvoker_); + } else if (name == ImageLoaderModule::kModuleName) { + return std::make_shared(jsInvoker_, imageLoader_); + } else if (name == SourceCodeModule::kModuleName) { + return std::make_shared( + jsInvoker_, sourceURL_ ? *sourceURL_ : ""); + } else if (name == WebSocketModule::kModuleName) { + return std::make_shared( + jsInvoker_, webSocketClientFactory_); + } else if (name == NativeExceptionsManager::kModuleName) { + return std::make_shared(onJsError_, jsInvoker_); + } else if (name == NativeIntersectionObserver::kModuleName) { + return std::make_shared(jsInvoker_); + } else if (name == NativeMutationObserver::kModuleName) { + return std::make_shared(jsInvoker_); + } else if (name == NetworkingModule::kModuleName) { + return std::make_shared(jsInvoker_, httpClientFactory_); + } else if (name == BlobModule::kModuleName) { + return std::make_shared(jsInvoker_); + } else if (name == FileReaderModule::kModuleName) { + return std::make_shared(jsInvoker_); + } else if (name == LogBoxModule::kModuleName) { + if (logBoxSurfaceDelegate_) { + return std::make_shared(jsInvoker_, logBoxSurfaceDelegate_); + } + } + + if (auto turboModule = + DefaultTurboModules::getTurboModule(name, jsInvoker_)) { + return turboModule; + } + + LOG(WARNING) << "Failed to load TurboModule: " << name; + return nullptr; +} diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.h new file mode 100644 index 0000000000..14f79e4e38 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactCxxTurboModuleProvider.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +class CallInvoker; +class IImageLoader; +class DevServerHelper; +class NativeAnimatedNodesManagerProvider; +class SurfaceDelegate; +struct IDevUIDelegate; + +class ReactCxxTurboModuleProvider final { + public: + ReactCxxTurboModuleProvider( + TurboModuleProviders turboModuleProviders, + std::shared_ptr jsInvoker, + JsErrorHandler::OnJsError onJsError, + std::shared_ptr animatedNodesManagerProvider = nullptr, + std::shared_ptr devServerHelper = nullptr, + std::shared_ptr devUIDelegate = nullptr, + std::shared_ptr logBoxSurfaceDelegate = nullptr, + HttpClientFactory httpClientFactory = nullptr, + WebSocketClientFactory webSocketClientFactory = nullptr, + std::function liveReloadCallback = nullptr, + std::shared_ptr sourceURL = nullptr, + std::weak_ptr imageLoader = {}); + + std::shared_ptr operator()(const std::string &name) const; + + private: + TurboModuleProviders turboModuleProviders_; + std::shared_ptr jsInvoker_; + JsErrorHandler::OnJsError onJsError_; + std::shared_ptr animatedNodesManagerProvider_; + std::shared_ptr devServerHelper_; + std::shared_ptr devUIDelegate_; + std::shared_ptr logBoxSurfaceDelegate_; + HttpClientFactory httpClientFactory_; + WebSocketClientFactory webSocketClientFactory_; + std::function liveReloadCallback_; + std::shared_ptr sourceURL_; + std::weak_ptr imageLoader_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactHost.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactHost.cpp new file mode 100644 index 0000000000..1027b9aa25 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -0,0 +1,557 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ReactHost.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ReactCxxTurboModuleProvider.h" + +namespace facebook::react { +struct ReactInstanceData { + std::shared_ptr mountingManager; + std::shared_ptr messageQueueThread; + std::shared_ptr runLoopObserverManager; + std::shared_ptr contextContainer; + ComponentRegistryFactory componentRegistryFactory; + JsErrorHandler::OnJsError onJsError; + Logger logger; + std::shared_ptr devUIDelegate; + TurboModuleProviders turboModuleProviders; + std::shared_ptr logBoxSurfaceDelegate; + std::shared_ptr + animatedNodesManagerProvider; + ReactInstance::BindingsInstallFunc bindingsInstallFunc; + std::shared_ptr animationChoreographer; +}; + +ReactHost::ReactHost( + ReactInstanceConfig reactInstanceConfig, + std::shared_ptr mountingManager, + std::shared_ptr runLoopObserverManager, + std::shared_ptr contextContainer, + JsErrorHandler::OnJsError onJsError, + Logger logger, + std::shared_ptr devUIDelegate, + TurboModuleProviders turboModuleProviders, + std::shared_ptr logBoxSurfaceDelegate, + std::shared_ptr + animatedNodesManagerProvider, + ReactInstance::BindingsInstallFunc bindingsInstallFunc, + std::shared_ptr animationChoreographer) + : reactInstanceConfig_(std::move(reactInstanceConfig)) { + auto componentRegistryFactory = + mountingManager->getComponentRegistryFactory(); + reactInstanceData_ = std::make_unique(ReactInstanceData{ + .mountingManager = mountingManager, + .messageQueueThread = nullptr, + .runLoopObserverManager = runLoopObserverManager, + .contextContainer = std::move(contextContainer), + .componentRegistryFactory = std::move(componentRegistryFactory), + .onJsError = std::move(onJsError), + .logger = std::move(logger), + .devUIDelegate = devUIDelegate, + .turboModuleProviders = std::move(turboModuleProviders), + .logBoxSurfaceDelegate = logBoxSurfaceDelegate, + .animatedNodesManagerProvider = animatedNodesManagerProvider, + .bindingsInstallFunc = std::move(bindingsInstallFunc), + .animationChoreographer = std::move(animationChoreographer)}); + if (!reactInstanceData_->contextContainer + ->find(MessageQueueThreadFactoryKey) + .has_value()) { + reactInstanceData_->contextContainer->insert( + MessageQueueThreadFactoryKey, MessageQueueThreadFactory([]() { + return std::make_shared(); + })); + } + if (!reactInstanceData_->contextContainer + ->find(HttpClientFactoryKey) + .has_value()) { + throw std::runtime_error("No HttpClientFactory provided"); + } + if (!reactInstanceData_->contextContainer + ->find(WebSocketClientFactoryKey) + .has_value()) { + throw std::runtime_error("No WebSocketClientFactory provided"); + } + createReactInstance(); +} + +ReactHost::~ReactHost() noexcept { + destroyReactInstance(); +} + +void ReactHost::createReactInstance() { + // A new JS instance is coming up. Bumping the generation here — the one place + // both the first load and a reload pass through — is what lets a caller wait + // for *this* instance rather than infer it from an empty tree. The increment + // happens before any mount, so a tree observed afterwards belongs to this + // instance and cannot be the previous one's. + instanceGeneration_.fetch_add(1, std::memory_order_relaxed); + + if (reactInstanceConfig_.animatedNodesManagerProviderFactory) { + reactInstanceData_->animatedNodesManagerProvider = + reactInstanceConfig_.animatedNodesManagerProviderFactory(); + } + // Set up timers + std::unique_ptr platformTimers; + if (reactInstanceConfig_.platformTimerRegistryFactory) { + platformTimers = reactInstanceConfig_.platformTimerRegistryFactory(); + } else { + platformTimers = std::make_unique(); + } + auto* platformTimersPtr = platformTimers.get(); + auto timerManager = std::make_shared(std::move(platformTimers)); + platformTimersPtr->setTimerManager(timerManager); + + auto httpClientFactory = + reactInstanceData_->contextContainer->at( + HttpClientFactoryKey); + + auto webSocketClientFactory = + reactInstanceData_->contextContainer->at( + WebSocketClientFactoryKey); + + auto devToolsHttpClientFactory = + reactInstanceData_->contextContainer + ->find(DevToolsHttpClientFactoryKey) + .value_or(httpClientFactory); + + auto devToolsWebSocketClientFactory = + reactInstanceData_->contextContainer + ->find(DevToolsWebSocketClientFactoryKey) + .value_or(webSocketClientFactory); + + // Create devServerHelper + if (!devServerHelper_ && + (reactInstanceConfig_.enableInspector || + reactInstanceConfig_.enableDevMode)) { + devServerHelper_ = std::make_shared( + reactInstanceConfig_.appId, + reactInstanceConfig_.deviceName, + reactInstanceConfig_.devServerHost, + reactInstanceConfig_.devServerPort, + devToolsHttpClientFactory, + [this]( + const std::string& moduleName, + const std::string& methodName, + folly::dynamic&& args) { + reactInstance_->callFunctionOnModule( + moduleName, methodName, std::move(args)); + }); + } + + if (!inspector_ && reactInstanceConfig_.enableInspector) { + inspector_ = std::make_shared( + reactInstanceConfig_.appId, + reactInstanceConfig_.deviceName, + devToolsWebSocketClientFactory, + devToolsHttpClientFactory); + inspector_->ensureHostTarget( + [this]() { reloadReactInstance(); }, + [weakDevUIDelegate = + std::weak_ptr(reactInstanceData_->devUIDelegate)]( + bool showDebuggerOverlay, + std::function&& resumeDebuggerFn) { + if (auto debugUIDelegate = weakDevUIDelegate.lock()) { + if (showDebuggerOverlay) { + debugUIDelegate->showDebuggerOverlay(std::move(resumeDebuggerFn)); + } else { + debugUIDelegate->hideDebuggerOverlay(); + } + } + }); + } + + if (!packagerConnection_ && reactInstanceConfig_.enableDevMode) { + packagerConnection_ = std::make_unique( + devToolsWebSocketClientFactory, + devServerHelper_->getPackagerConnectionUrl(), + [this]() { reloadReactInstance(); }, + []() {}); + } + + // Create the React Instance + auto messageQueueThreadFactory = + reactInstanceData_->contextContainer->at( + MessageQueueThreadFactoryKey); + reactInstanceData_->messageQueueThread = messageQueueThreadFactory(); + if (reactInstanceConfig_.jsRuntimeFactory == nullptr) { + throw std::runtime_error("No JSRuntimeFactory provided"); + } + std::unique_ptr runtime = + reactInstanceConfig_.jsRuntimeFactory->createJSRuntime( + reactInstanceData_->messageQueueThread); + + if (reactInstanceConfig_.enableInspector) { + react_native_assert( + inspector_ != nullptr && "Inspector is not initialized"); + } + reactInstance_ = std::make_unique( + std::move(runtime), + reactInstanceData_->messageQueueThread, + timerManager, + reactInstanceData_->onJsError, + inspector_ != nullptr ? inspector_->inspectorTarget().get() : nullptr); + timerManager->setRuntimeExecutor( + reactInstance_->getBufferedRuntimeExecutor()); + reactInstanceData_->contextContainer->insert( + RuntimeSchedulerKey, + std::weak_ptr(reactInstance_->getRuntimeScheduler())); + + // Create scheduler + auto toolbox = SchedulerToolbox{}; + toolbox.contextContainer = reactInstanceData_->contextContainer; + toolbox.runtimeExecutor = reactInstance_->getBufferedRuntimeExecutor(); + toolbox.bridgelessBindingsExecutor = + reactInstance_->getUnbufferedRuntimeExecutor(); + toolbox.componentRegistryFactory = + reactInstanceData_->componentRegistryFactory; + toolbox.eventBeatFactory = + [runLoopObserverManager = reactInstanceData_->runLoopObserverManager, + runtimeScheduler = reactInstance_->getRuntimeScheduler()]( + std::shared_ptr ownerBox) { + return runLoopObserverManager->createEventBeat( + ownerBox, *runtimeScheduler); + }; + toolbox.animationChoreographer = reactInstanceData_->animationChoreographer; + + auto schedulerDelegate = std::make_unique( + reactInstanceData_->mountingManager); + scheduler_ = + std::make_unique(toolbox, nullptr, schedulerDelegate.get()); + + surfaceManager_ = std::make_unique(*scheduler_); + schedulerDelegate->setUIManager(scheduler_->getUIManager()); + schedulerDelegate_ = std::move(schedulerDelegate); + + reactInstanceData_->mountingManager->setSchedulerTaskExecutor( + [this](SchedulerTask&& task) { runOnScheduler(std::move(task)); }); + + reactInstanceData_->mountingManager->setUIManager(scheduler_->getUIManager()); + + auto jsInvoker = std::make_shared( + reactInstance_->getRuntimeScheduler()); + + if (inspector_ != nullptr) { + inspector_->connectDebugger(devServerHelper_->getInspectorUrl()); + } + + auto liveReloadCallback = [this]() { reloadReactInstance(); }; + ReactCxxTurboModuleProvider turboModuleManager( + reactInstanceData_->turboModuleProviders, + jsInvoker, + reactInstanceData_->onJsError, + reactInstanceData_->animatedNodesManagerProvider, + reactInstanceConfig_.enableDevMode ? devServerHelper_ : nullptr, + reactInstanceData_->devUIDelegate, + reactInstanceData_->logBoxSurfaceDelegate, + httpClientFactory, + webSocketClientFactory, + std::move(liveReloadCallback), + sourceURL_, + reactInstanceData_->mountingManager->getImageLoader()); + + reactInstance_->initializeRuntime( + { +#if defined(WITH_PERFETTO) || defined(RNCXX_WITH_PROFILING_PROVIDER) + .isProfiling = true, +#else + .isProfiling = false, +#endif + .runtimeDiagnosticFlags = ""}, + [weakMountingManager = + std::weak_ptr(reactInstanceData_->mountingManager), + logger = reactInstanceData_->logger, + bindingsInstallFunc = reactInstanceData_->bindingsInstallFunc, + turboModuleManager = + std::move(turboModuleManager)](jsi::Runtime& runtime) mutable { + if (logger) { + bindNativeLogger(runtime, logger); + } + + // Set up component provider + bindHasComponentProvider( + runtime, [weakMountingManager](const std::string& name) -> bool { + if (auto strongMountingManager = weakMountingManager.lock()) { + return strongMountingManager->hasComponent(name); + } + return false; + }); + + // Set up TurboModules + TurboModuleBinding::install(runtime, std::move(turboModuleManager)); + + if (bindingsInstallFunc) { + bindingsInstallFunc(runtime); + } + }); +} + +void ReactHost::destroyReactInstance() { + if (reactInstanceConfig_.beforeInstanceDestroy) { + reactInstanceConfig_.beforeInstanceDestroy(); + } + if (inspector_ != nullptr) { + reactInstance_->unregisterFromInspector(); + } + if (surfaceManager_) { + surfaceManager_->stopAllSurfaces(); + } + reactInstanceData_->messageQueueThread->quitSynchronous(); + surfaceManager_ = nullptr; + scheduler_ = nullptr; + schedulerDelegate_ = nullptr; + + reactInstanceData_->contextContainer->erase(RuntimeSchedulerKey); + reactInstanceData_->mountingManager->setSchedulerTaskExecutor(nullptr); + reactInstance_ = nullptr; + reactInstanceData_->messageQueueThread = nullptr; +} + +void ReactHost::reloadReactInstance() { + if (isReloadingReactInstance_.exchange(true)) { + return; + } + + std::thread([this]() { + folly::setThreadName("ReactReload"); + + std::vector surfaceProps; + for (auto& surfaceId : surfaceManager_->getRunningSurfaces()) { + if (auto surfaceProp = surfaceManager_->getSurfaceProps(surfaceId); + surfaceProp.has_value()) { + surfaceProps.emplace_back(*surfaceProp); + } + } + destroyReactInstance(); + { + std::optional> devServer; + { + std::lock_guard lock(pendingDevServerMutex_); + devServer.swap(pendingDevServer_); + } + if (devServer && devServerHelper_) { + // Both connections were opened to the old server and are kept across + // reloads; dropping them makes `createReactInstance` open new ones. + devServerHelper_->setDevServer(devServer->first, devServer->second); + packagerConnection_.reset(); + if (inspector_ != nullptr) { + inspector_->disconnectDebugger(); + } + } + } + createReactInstance(); + loadScript(lastBundlePath_, lastSourcePath_); + for (auto& surfaceProp : surfaceProps) { + startSurface( + surfaceProp.surfaceId, + surfaceProp.moduleName, + surfaceProp.props, + surfaceProp.layoutConstraints, + surfaceProp.layoutContext); + } + isReloadingReactInstance_ = false; + }).detach(); +} + +bool ReactHost::setDevServer(const std::string& host, uint32_t port) { + if (!devServerHelper_ || host.empty() || port == 0) { + return false; + } + { + std::lock_guard lock(pendingDevServerMutex_); + pendingDevServer_ = std::make_pair(host, port); + } + reloadReactInstance(); + return true; +} + +std::string ReactHost::devServerAddress() const { + if (!devServerHelper_) { + return ""; + } + return devServerHelper_->devServerHost() + ":" + + std::to_string(devServerHelper_->devServerPort()); +} + +bool ReactHost::loadScript( + const std::string& bundlePath, + const std::string& sourcePath) noexcept { + lastBundlePath_ = bundlePath; + lastSourcePath_ = sourcePath; + bool isLoaded = false; + if (reactInstanceConfig_.enableDevMode && devServerHelper_) { + devServerHelper_->setSourcePath(sourcePath); + isLoaded = loadScriptFromDevServer(); + } + if (!isLoaded) { + LOG(INFO) << "Devserver is not provided."; + isLoaded = loadScriptFromBundlePath(bundlePath); + } + return isLoaded; +} + +void ReactHost::openDebugger() { + if (inspector_ != nullptr && devServerHelper_ != nullptr) { + devServerHelper_->openDebugger(); + } +} + +bool ReactHost::loadScriptFromDevServer() { + try { + auto bundleUrl = devServerHelper_->getBundleUrl(); + auto response = + devServerHelper_ + ->downloadBundleResourceSync( + bundleUrl, + [weakDevUIDelegate = std::weak_ptr( + reactInstanceData_->devUIDelegate)]( + DevServerHelper::DownloadProgressStatus status) { + if (auto devUIDelegate = weakDevUIDelegate.lock()) { + if (status == + DevServerHelper::DownloadProgressStatus::STARTED) { + devUIDelegate->showDownloadBundleProgress(); + } else { + devUIDelegate->hideDownloadBundleProgress(); + } + } + }) + .get(); + auto script = std::make_unique(std::move(response)); + *sourceURL_ = bundleUrl; + reactInstance_->loadScript(std::move(script), bundleUrl); + devServerHelper_->setupHMRClient(); + return true; + } catch (const std::exception& error) { + // The reason is logged where it is known (no server, a failed request, or + // a response that is not a bundle); this says what happens next. + LOG(WARNING) << "Not using the dev server (" << error.what() + << "); loading the built-in JS bundle instead. Start Metro " + << "for this app to get live reload."; + return false; + } catch (...) { + LOG(WARNING) << "Not using the dev server; loading the built-in JS bundle " + << "instead. Start Metro for this app to get live reload."; + return false; + } +} + +bool ReactHost::loadScriptFromBundlePath(const std::string& bundlePath) { + try { + LOG(INFO) << "Loading JS bundle from bundle path: " << bundlePath; + auto script = ResourceLoader::getFileContents(bundlePath); + *sourceURL_ = ""; + reactInstance_->loadScript(std::move(script), bundlePath); + LOG(INFO) << "Loaded JS bundle from bundle path: " << bundlePath; + return true; + } catch (...) { + LOG(WARNING) << "Unable to read bundle from bundle path" << bundlePath; + return false; + } +} + +void ReactHost::startSurface( + SurfaceId surfaceId, + const std::string& moduleName, + const folly::dynamic& initialProps, + const LayoutConstraints& layoutConstraints, + const LayoutContext& layoutContext) noexcept { + surfaceManager_->startSurface( + surfaceId, moduleName, initialProps, layoutConstraints, layoutContext); +} + +void ReactHost::setSurfaceConstraints( + SurfaceId surfaceId, + const LayoutConstraints& layoutConstraints, + const LayoutContext& layoutContext) noexcept { + surfaceManager_->constraintSurfaceLayout( + surfaceId, layoutConstraints, layoutContext); +} + +void ReactHost::stopSurface(SurfaceId surfaceId) noexcept { + surfaceManager_->stopSurface(surfaceId); +} + +void ReactHost::stopAllSurfaces() noexcept { + surfaceManager_->stopAllSurfaces(); +} + +bool ReactHost::isSurfaceRunning(SurfaceId surfaceId) const noexcept { + return surfaceManager_->isSurfaceRunning(surfaceId); +} + +std::unordered_set ReactHost::getRunningSurfaces() const noexcept { + return surfaceManager_->getRunningSurfaces(); +} + +std::optional ReactHost::surfaceContentSize( + SurfaceId surfaceId) const { + if (isReloadingReactInstance_ || scheduler_ == nullptr) { + return std::nullopt; + } + const auto uiManager = scheduler_->getUIManager(); + if (uiManager == nullptr) { + return std::nullopt; + } + const auto root = + uiManager->getShadowTreeRevisionProvider()->getCurrentRevision(surfaceId); + if (root == nullptr) { + return std::nullopt; + } + const auto& frame = root->getLayoutMetrics().frame; + return Size{frame.size.width, frame.size.height}; +} + +void ReactHost::runOnScheduler( + std::function&& task) const { + if (!isReloadingReactInstance_) { + task(*scheduler_); + } +} + +void ReactHost::runOnRuntimeScheduler( + std::function&& task, + SchedulerPriority priority) const noexcept { + if (!isReloadingReactInstance_) { + reactInstance_->getRuntimeScheduler()->scheduleTask( + priority, std::move(task)); + } +} + +void ReactHost::emitDeviceEvent(folly::dynamic&& args) { + if (!isReloadingReactInstance_) { + reactInstance_->callFunctionOnModule( + "RCTDeviceEventEmitter", "emit", std::move(args)); + } +} +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactHost.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactHost.h new file mode 100644 index 0000000000..1f67bfa164 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactHost.h @@ -0,0 +1,150 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "ReactInstanceConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { +class AnimatedMountingOverrideDelegate; +class DevServerHelper; +class IMountingManager; +class Inspector; +class NativeAnimatedNodesManagerProvider; +class PackagerConnection; +class SchedulerDelegate; +class SurfaceDelegate; +class SurfaceManager; +struct IDevUIDelegate; +struct ReactInstanceData; + +class ReactHost { + public: + ReactHost( + ReactInstanceConfig reactInstanceConfig, + std::shared_ptr mountingManager, + std::shared_ptr runLoopObserverManager, + std::shared_ptr contextContainer, + JsErrorHandler::OnJsError onJsError, + Logger logger, + std::shared_ptr devUIDelegate = nullptr, + TurboModuleProviders turboModuleProviders = {}, + std::shared_ptr logBoxSurfaceDelegate = nullptr, + std::shared_ptr animatedNodesManagerProvider = nullptr, + ReactInstance::BindingsInstallFunc bindingsInstallFunc = nullptr, + std::shared_ptr animationChoreographer = nullptr); + ReactHost(const ReactHost &) = delete; + ReactHost &operator=(const ReactHost &) = delete; + ReactHost(ReactHost &&) noexcept = delete; + ReactHost &operator=(ReactHost &&) noexcept = delete; + ~ReactHost() noexcept; + + bool loadScript(const std::string &bundlePath, const std::string &sourcePath) noexcept; + + void openDebugger(); + void requestReload() { reloadReactInstance(); } + bool isReloading() const { return isReloadingReactInstance_.load(); } + + /// Monotonic count of the React instances this host has created, starting at + /// 1 for the first. A reload destroys the old instance and creates a new one, + /// so a test can tell "the tree is empty because the old instance is being + /// torn down" from "the tree is empty because the new one has not mounted + /// yet" by watching this change, instead of inferring it from an empty tree. + uint64_t instanceGeneration() const { + return instanceGeneration_.load(std::memory_order_relaxed); + } + + void startSurface( + SurfaceId surfaceId, + const std::string &moduleName /* can be empty */, + const folly::dynamic &initialProps, + const LayoutConstraints &layoutConstraints, + const LayoutContext &layoutContext = {}) noexcept; + + void setSurfaceConstraints( + SurfaceId surfaceId, + const LayoutConstraints &layoutConstraints, + const LayoutContext &layoutContext) noexcept; + + void stopSurface(SurfaceId surfaceId) noexcept; + + void stopAllSurfaces() noexcept; + + bool isSurfaceRunning(SurfaceId surfaceId) const noexcept; + + std::unordered_set getRunningSurfaces() const noexcept; + + void runOnScheduler(std::function &&task) const; + + std::optional surfaceContentSize(SurfaceId surfaceId) const; + + void runOnRuntimeScheduler( + std::function &&task, + SchedulerPriority priority = SchedulerPriority::NormalPriority) const noexcept; + + void emitDeviceEvent(folly::dynamic &&args); + + // react-native-gpui: the Dev Menu's "Configure Bundler". Points the dev + // server connections at `host:port` and reloads from there. Returns false + // when this build has no dev server (dev mode off). + bool setDevServer(const std::string &host, uint32_t port); + // "host:port" of the dev server in use, or empty without one. + std::string devServerAddress() const; + + private: + void createReactInstance(); + void destroyReactInstance(); + void reloadReactInstance(); + + bool loadScriptFromDevServer(); + bool loadScriptFromBundlePath(const std::string &bundlePath); + + std::string lastBundlePath_; + std::string lastSourcePath_; + const ReactInstanceConfig reactInstanceConfig_; + std::unique_ptr reactInstanceData_; + std::unique_ptr reactInstance_; + std::atomic isReloadingReactInstance_{false}; + /// Incremented by `createReactInstance`, so it is 1 for the first instance and + /// one more after each reload. See `instanceGeneration`. + std::atomic instanceGeneration_{0}; + + std::unique_ptr schedulerDelegate_; + std::unique_ptr scheduler_; + std::unique_ptr surfaceManager_; + + std::shared_ptr devServerHelper_; + std::shared_ptr sourceURL_ = std::make_shared(); + std::shared_ptr inspector_; + std::unique_ptr packagerConnection_; + // Applied by the next reload, between the old instance and the new one. + std::mutex pendingDevServerMutex_; + std::optional> pendingDevServer_; + + std::shared_ptr animatedMountingOverrideDelegate_; +}; +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactInstanceConfig.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactInstanceConfig.h new file mode 100644 index 0000000000..d8be9d5e63 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/runtime/ReactInstanceConfig.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +class JSRuntimeFactory; +class PlatformTimerRegistry; +class NativeAnimatedNodesManagerProvider; + +struct ReactInstanceConfig { + std::string appId; + std::string deviceName; +#ifdef REACT_NATIVE_DEBUG + bool enableDevMode{true}; + bool enableInspector{true}; +#else + bool enableDevMode{false}; + bool enableInspector{false}; +#endif + std::string devServerHost{"localhost"}; + uint32_t devServerPort{8081}; + + // Factory used to create the JS runtime for every (re)load. Supplied by the + // active JS engine package; ReactHost throws when it is null. + std::shared_ptr jsRuntimeFactory{nullptr}; + + // Optional factory used to create the PlatformTimerRegistry for the instance. + // When unset, a default thread-based PlatformTimerRegistryImpl is used. This + // is a seam for tests (e.g. Fantom) to inject a deterministic, mockable timer + // registry. + std::function()> platformTimerRegistryFactory{nullptr}; + + // Native animation graphs hold UIManager/runtime references and must not be + // reused across a reload. Platforms with render callbacks create a provider + // per instance and quiesce those callbacks before the instance is destroyed. + std::function()> + animatedNodesManagerProviderFactory{nullptr}; + std::function beforeInstanceDestroy{nullptr}; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/CMakeLists.txt new file mode 100644 index 0000000000..5165780045 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_threading_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_cxx_platform_react_threading OBJECT ${react_cxx_platform_react_threading_SRC}) + +target_include_directories(react_cxx_platform_react_threading PUBLIC ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_threading + folly_runtime + react_cxxreact +) +target_compile_reactnative_options(react_cxx_platform_react_threading PRIVATE) +target_compile_options(react_cxx_platform_react_threading PRIVATE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/MessageQueueThreadImpl.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/MessageQueueThreadImpl.cpp new file mode 100644 index 0000000000..abc9af9b36 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/MessageQueueThreadImpl.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "MessageQueueThreadImpl.h" + +#include +#include +#include + +namespace { +std::atomic pendingJsTasks{0}; +struct PendingTask { + PendingTask() { ++pendingJsTasks; } + ~PendingTask() { --pendingJsTasks; } +}; +std::function tracked(std::function runnable) { + auto activity = std::make_shared(); + return [activity, runnable = std::move(runnable)]() { runnable(); }; +} +} +extern "C" uint64_t rn_gpui_pending_js_tasks() noexcept { + return pendingJsTasks.load(); +} + +namespace facebook::react { + +void MessageQueueThreadImpl::runOnQueue(std::function&& runnable) { + if (!taskDispatchThread_.isRunning()) { + return; + } + taskDispatchThread_.runAsync(tracked(std::move(runnable))); +} + +void MessageQueueThreadImpl::runOnQueueSync(std::function&& runnable) { + if (!taskDispatchThread_.isRunning()) { + return; + } + if (taskDispatchThread_.isOnThread()) { + tracked(std::move(runnable))(); + } else { + taskDispatchThread_.runSync(tracked(std::move(runnable))); + } +} + +void MessageQueueThreadImpl::quitSynchronous() { + taskDispatchThread_.quit(); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/MessageQueueThreadImpl.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/MessageQueueThreadImpl.h new file mode 100644 index 0000000000..68b9dff74b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/MessageQueueThreadImpl.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +using MessageQueueThreadFactory = std::function()>; + +constexpr char MessageQueueThreadFactoryKey[] = "MessageQueueThreadFactoryKey"; + +/** + * MessageQueueThread implementation that uses a TaskDispatchThread for + * queueing and threading logic + */ +class MessageQueueThreadImpl : public MessageQueueThread { + public: + MessageQueueThreadImpl() noexcept = default; + explicit MessageQueueThreadImpl(int priorityOffset) noexcept : taskDispatchThread_("MessageQueue", priorityOffset) {} + + ~MessageQueueThreadImpl() noexcept override = default; + + /** Add a job to the queue asynchronously */ + void runOnQueue(std::function &&runnable) override; + + /** + * Add a job to the queue synchronously - call won't return until runnable + * has completed. Will run immediately if called from the looper thread. + * Should only be used for initialization. + */ + void runOnQueueSync(std::function &&runnable) override; + + /** + * Stop the message queue thread. Should only be used for cleanup - once it + * returns, no further work should run on the queue. + */ + void quitSynchronous() override; + + private: + TaskDispatchThread taskDispatchThread_{"MessageQueue"}; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/TaskDispatchThread.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/TaskDispatchThread.cpp new file mode 100644 index 0000000000..c8298edbf4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/TaskDispatchThread.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "TaskDispatchThread.h" + +#include +#include +#include +#include +#include +#include + +#ifdef ANDROID +#include +#include +#include +#endif + +namespace facebook::react { + +TaskDispatchThread::TaskDispatchThread( + std::string_view threadName, + int priorityOffset) noexcept + : threadName_(threadName) { +#ifdef ANDROID + // Attaches the thread to JVM just in case anything calls out to Java + thread_ = std::thread([&]() { + facebook::jni::ThreadScope::WithClassLoader([&]() { + int result = setpriority( + PRIO_PROCESS, + static_cast(::syscall(SYS_gettid)), + priorityOffset); + + if (result != 0) { + LOG(INFO) << " setCurrentThreadPriority failed with pri errno: " + << errno; + } + + loop(); + }); + }); + +#else + thread_ = std::thread(&TaskDispatchThread::loop, this); +#endif +} + +TaskDispatchThread::~TaskDispatchThread() noexcept { + quit(); + + if (thread_.joinable()) { + thread_.join(); + } +} + +bool TaskDispatchThread::isOnThread() noexcept { + return std::this_thread::get_id() == thread_.get_id(); +} + +bool TaskDispatchThread::isRunning() noexcept { + return running_; +} + +void TaskDispatchThread::runAsync( + TaskFn&& task, + std::chrono::milliseconds delayMs) noexcept { + std::lock_guard guard(mutex_); + if (!running_) { + return; + } + auto dispatchTime = std::chrono::system_clock::now() + delayMs; + queue_.emplace(dispatchTime, std::move(task)); + loopCv_.notify_one(); +} + +void TaskDispatchThread::runSync(TaskFn&& task) noexcept { + // Calling runSync from the dispatch thread would deadlock, as the thread + // would wait for itself to execute the task it just queued. + assert(!isOnThread()); + + // Use shared_ptr to ensure the promise outlives both the caller and the + // queued task. The destructor guard ensures the promise is fulfilled even + // if the task is destroyed without executing (e.g., when quit() clears the + // queue during shutdown). + auto promise = std::make_shared>(); + auto guard = std::shared_ptr( + nullptr, [promise](void*) { promise->set_value(); }); + + runAsync([guard = std::move(guard), task = std::move(task)]() { task(); }); + promise->get_future().wait(); +} + +void TaskDispatchThread::quit() noexcept { + { + /* + We should hold lock even when changing atomic variable to correctly + publish the modificatino to the waiting thread + from: https://en.cppreference.com/w/cpp/thread/condition_variable.html + */ + std::unique_lock lock(mutex_); + if (!running_.exchange(false)) { + return; + } + } + + loopCv_.notify_one(); + if (!isOnThread()) { + loopStoppedPromise_.get_future().wait(); + } +} + +void TaskDispatchThread::loop() noexcept { + if (!threadName_.empty()) { + folly::setThreadName(threadName_); + } + while (running_) { + std::unique_lock lock(mutex_); + loopCv_.wait(lock, [&]() { return !running_ || !queue_.empty(); }); + + while (!queue_.empty()) { + auto task = queue_.top(); + auto now = std::chrono::system_clock::now(); + + if (running_) { + if (task.dispatchTime > now) { + // Wait until the scheduled task time, if delayed + loopCv_.wait_until(lock, task.dispatchTime); + continue; + } + } else { + // Shutting down, skip all the remaining tasks + queue_ = {}; + break; + } + + // We should check whether the task thread is still running at this + // point (which may not anymore be the case since the previous check) + if (running_) { + lock.unlock(); + task.fn(); + lock.lock(); + } + queue_.pop(); + } + } + + loopStoppedPromise_.set_value(); +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/TaskDispatchThread.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/TaskDispatchThread.h new file mode 100644 index 0000000000..2dd86991b9 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/TaskDispatchThread.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +/** + * Representation of a thread looper which can add tasks to a queue and handle + * the synchronization of callers. + */ +class TaskDispatchThread { + public: + using TaskFn = std::function; + using TimePoint = std::chrono::time_point; + + TaskDispatchThread(std::string_view threadName = "", int priorityOffset = 0) noexcept; + + ~TaskDispatchThread() noexcept; + + /** Return true if the current thread is the same as this looper's thread. */ + bool isOnThread() noexcept; + + /** Return true until TaskDispatchThread.quit() is called */ + bool isRunning() noexcept; + + /** Add task to the queue and return immediately. */ + void runAsync(TaskFn &&task, std::chrono::milliseconds delayMs = std::chrono::milliseconds::zero()) noexcept; + + /** Add task to the queue and wait until it has completed. */ + void runSync(TaskFn &&task) noexcept; + + /** Shut down and clean up the thread. */ + void quit() noexcept; + + protected: + struct Task { + TimePoint dispatchTime; + TaskFn fn; + + Task(TimePoint dispatchTime, TaskFn &&fn) : dispatchTime(dispatchTime), fn(std::move(fn)) {} + + bool operator<(const Task &other) const + { + // Have the earliest tasks be at the front of the queue. + return dispatchTime > other.dispatchTime; + } + }; + + void loop() noexcept; + + std::mutex mutex_; + std::condition_variable loopCv_; + std::priority_queue queue_; + std::atomic running_{true}; + std::string threadName_; + std::thread thread_; + + std::promise loopStoppedPromise_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/tests/.clang-tidy b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/tests/.clang-tidy new file mode 100644 index 0000000000..700d6cc363 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/tests/.clang-tidy @@ -0,0 +1,5 @@ +--- +InheritParentConfig: true +Checks: ' +-facebook-hte-BadCall-sleep_for, +' diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/tests/TaskDispatchThreadTests.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/tests/TaskDispatchThreadTests.cpp new file mode 100644 index 0000000000..99e35cd516 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/threading/tests/TaskDispatchThreadTests.cpp @@ -0,0 +1,207 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class TaskDispatchThreadTest : public ::testing::Test { + protected: + std::unique_ptr dispatcher{ + std::make_unique()}; +}; + +// Test: isOnThread returns true inside the looper thread +TEST_F(TaskDispatchThreadTest, IsOnThreadReturnsTrueInLooper) { + bool result = false; + dispatcher->runSync([&] { result = dispatcher->isOnThread(); }); + EXPECT_TRUE(result); +} + +// Test: isRunning returns true before quit, false after +TEST_F(TaskDispatchThreadTest, IsRunningFlag) { + EXPECT_TRUE(dispatcher->isRunning()); + dispatcher->quit(); + EXPECT_FALSE(dispatcher->isRunning()); +} + +// Test: runAsync executes the task +TEST_F(TaskDispatchThreadTest, RunAsyncExecutesTask) { + std::atomic counter{0}; + dispatcher->runAsync([&] { counter++; }); + // Wait for task to complete + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_EQ(counter.load(), 1); +} + +// Test: runSync executes the task and blocks until done +TEST_F(TaskDispatchThreadTest, RunSyncExecutesTask) { + std::atomic counter{0}; + dispatcher->runSync([&] { counter++; }); + EXPECT_EQ(counter.load(), 1); +} + +// Test: runAsync with delay +TEST_F(TaskDispatchThreadTest, RunAsyncWithDelay) { + std::atomic counter{0}; + std::promise taskDone; + auto future = taskDone.get_future(); + dispatcher->runAsync( + [&] { + counter++; + taskDone.set_value(); + }, + std::chrono::milliseconds(200)); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + EXPECT_EQ(counter.load(), 0); // 20ms << 200ms, not yet executed + ASSERT_EQ( + future.wait_for(std::chrono::seconds(5)), std::future_status::ready); + EXPECT_EQ(counter.load(), 1); // Task completed +} + +// Test: Multiple delayed tasks execute in order +TEST_F(TaskDispatchThreadTest, MultipleDelayedTasksOrder) { + std::vector results; + dispatcher->runAsync( + [&] { results.push_back(1); }, std::chrono::milliseconds(50)); + dispatcher->runAsync( + [&] { results.push_back(2); }, std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(120)); + ASSERT_EQ(results.size(), 2); + EXPECT_EQ(results[0], 1); + EXPECT_EQ(results[1], 2); +} + +// Test: runSync blocks until task is done +TEST_F(TaskDispatchThreadTest, RunSyncBlocksUntilDone) { + std::atomic started{false}; + std::atomic finished{false}; + dispatcher->runAsync([&] { + started = true; + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + finished = true; + }); + dispatcher->runSync([&] { + EXPECT_TRUE(started); + EXPECT_TRUE(finished); + }); +} + +// Test: quit prevents further tasks from running +TEST_F(TaskDispatchThreadTest, QuitPreventsFurtherTasks) { + dispatcher->quit(); + std::atomic counter{0}; + dispatcher->runAsync([&] { counter++; }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + EXPECT_EQ(counter.load(), 0); +} + +// Test: Multiple runSync tasks execute serially +TEST_F(TaskDispatchThreadTest, MultipleRunSyncSerialExecution) { + std::vector results; + dispatcher->runSync([&] { results.push_back(1); }); + dispatcher->runSync([&] { results.push_back(2); }); + EXPECT_EQ(results.size(), 2); + EXPECT_EQ(results[0], 1); + EXPECT_EQ(results[1], 2); +} + +// Test: Edge case - runSync after quit should not execute +TEST_F(TaskDispatchThreadTest, RunSyncAfterQuitDoesNotExecute) { + dispatcher->quit(); + std::atomic counter{0}; + dispatcher->runSync([&] { counter++; }); + EXPECT_EQ(counter.load(), 0); +} + +// Test: Thread safety - runAsync from multiple threads +TEST_F(TaskDispatchThreadTest, RunAsyncFromMultipleThreads) { + std::atomic counter{0}; + auto task = [&] { dispatcher->runAsync([&] { counter++; }); }; + std::thread t1(task); + std::thread t2(task); + std::thread t3(task); + t1.join(); + t2.join(); + t3.join(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + EXPECT_EQ(counter.load(), 3); +} + +// Test: quit() shouldn't block if it is called inside loop thread +TEST_F(TaskDispatchThreadTest, QuitInTaskShouldntBeBlockedForever) { + dispatcher->runSync([&] { dispatcher->quit(); }); +} + +// Test: quit() should wait for already running task in the thread +TEST_F(TaskDispatchThreadTest, QuitShouldWaitAlreadyRunningTask) { + { + std::unique_ptr counter = std::make_unique(0); + dispatcher->runAsync([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + *counter = 1; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + // if quit doesn't wait for running task, then *counter will access already + // deleted object + dispatcher->quit(); + } + // forcing dispatcher to join thread + dispatcher.reset(); +} + +// Test: sync tasks shouldn't be blocked for forever due to delayed task +TEST_F(TaskDispatchThreadTest, SyncTaskShouldntBeBlockedDueToDelayedTask) { + std::atomic counter = 0; + constexpr int kHugeDelay = 100000; + dispatcher->runAsync([&] {}, std::chrono::seconds(kHugeDelay)); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + dispatcher->runSync([&] { counter++; }); + EXPECT_EQ(counter.load(), 1); +} + +// Test: runSync should not block forever when quit() is called concurrently +// This tests the fix where the RAII guard ensures the promise is fulfilled +// even if the task is destroyed without executing during shutdown. +TEST_F(TaskDispatchThreadTest, RunSyncDoesNotBlockWhenQuitCalledConcurrently) { + for (int i = 0; i < 100; ++i) { + auto localDispatcher = std::make_unique(); + + std::atomic runSyncCompleted{false}; + + // Block the dispatcher thread with a slow task + localDispatcher->runAsync( + [&] { std::this_thread::sleep_for(std::chrono::milliseconds(10)); }); + + // Start runSync from another thread - it will queue a task and wait + std::thread syncThread([&] { + localDispatcher->runSync([&] {}); + runSyncCompleted = true; + }); + + // Concurrently call quit() - this should cause the queued task to be + // destroyed without executing, but runSync should still return (not block + // forever) because the RAII guard fulfills the promise on destruction. + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + localDispatcher->quit(); + + // If the fix works, syncThread should complete. If not, this would hang. + syncThread.join(); + + // runSync should have completed (either task ran or was skipped due to + // shutdown) + EXPECT_TRUE(runSyncCompleted.load()); + + localDispatcher.reset(); + } +} +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/CMakeLists.txt new file mode 100644 index 0000000000..024447be3d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +file(GLOB react_cxx_platform_react_utils_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_cxx_platform_react_utils OBJECT ${react_cxx_platform_react_utils_SRC}) + +target_include_directories(react_cxx_platform_react_utils PUBLIC ${REACT_CXX_PLATFORM_DIR}) + +target_link_libraries(react_cxx_platform_react_utils + react_debug + react_renderer_core + react_utils +) +target_compile_reactnative_options(react_cxx_platform_react_utils PRIVATE) +target_compile_options(react_cxx_platform_react_utils PRIVATE -Wpedantic) diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/PlatformRunLoopObserver.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/PlatformRunLoopObserver.h new file mode 100644 index 0000000000..69cafe5632 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/PlatformRunLoopObserver.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +class PlatformRunLoopObserver; + +class PlatformRunLoopObserver : public RunLoopObserver { + public: + PlatformRunLoopObserver(RunLoopObserver::Activity activities, const RunLoopObserver::WeakOwner &owner) + : RunLoopObserver(activities, owner) + { + } + ~PlatformRunLoopObserver() override = default; + + PlatformRunLoopObserver(PlatformRunLoopObserver &other) = delete; + PlatformRunLoopObserver &operator=(PlatformRunLoopObserver &other) = delete; + PlatformRunLoopObserver(PlatformRunLoopObserver &&other) = delete; + PlatformRunLoopObserver &operator=(PlatformRunLoopObserver &&other) = delete; + + bool isOnRunLoopThread() const noexcept override + { + return false; + } + + void onRender() const noexcept + { + if (auto owner = owner_.lock()) { + activityDidChange(activities_); + } + } + + private: + void startObserving() const noexcept override {} + void stopObserving() const noexcept override {} +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/RunLoopObserverManager.cpp b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/RunLoopObserverManager.cpp new file mode 100644 index 0000000000..cd02314d27 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/RunLoopObserverManager.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "RunLoopObserverManager.h" + +#include + +namespace facebook::react { + +/* + * Event beat associated with JavaScript runtime. The beat is called on + * RuntimeScheduler's thread induced by the UI thread event loop. + */ +class EventBeatImpl : public EventBeat, public RunLoopObserver::Delegate { + public: + EventBeatImpl( + std::shared_ptr ownerBox, + std::shared_ptr uiRunLoopObserver, + RuntimeScheduler& runtimeScheduler) + : EventBeat(std::move(ownerBox), runtimeScheduler), + uiRunLoopObserver_(std::move(uiRunLoopObserver)) { + uiRunLoopObserver_->setDelegate(this); + uiRunLoopObserver_->enable(); + } + +#pragma mark - RunLoopObserver::Delegate + + void activityDidChange( + const RunLoopObserver::Delegate* delegate, + RunLoopObserver::Activity /*activity*/) const noexcept override { + react_native_assert(delegate == this); + induce(); + } + + private: + std::shared_ptr uiRunLoopObserver_; +}; + +std::unique_ptr RunLoopObserverManager::createEventBeat( + std::shared_ptr ownerBox, + RuntimeScheduler& runtimeScheduler) { + auto observer = std::make_shared( + RunLoopObserver::Activity::BeforeWaiting, ownerBox->owner); + observer_ = observer; + return std::make_unique( + std::move(ownerBox), std::move(observer), runtimeScheduler); +} + +void RunLoopObserverManager::onRender() const noexcept { + if (auto observer = observer_.lock()) { + observer->onRender(); + } +} + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/RunLoopObserverManager.h b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/RunLoopObserverManager.h new file mode 100644 index 0000000000..f7b0f241e9 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactCxxPlatform/react/utils/RunLoopObserverManager.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include "PlatformRunLoopObserver.h" + +#include +#include +#include + +namespace facebook::react { + +class RunLoopObserverManager : std::enable_shared_from_this { + public: + std::unique_ptr createEventBeat( + std::shared_ptr ownerBox, + RuntimeScheduler &runtimeScheduler); + + void onRender() const noexcept; + + void induce() const noexcept; + + private: + std::weak_ptr observer_; +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/Cargo.lock b/vendor/react-native-gpui/packages/core/ReactGpui/Cargo.lock new file mode 100644 index 0000000000..9fde054949 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/Cargo.lock @@ -0,0 +1,7775 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "enumn", + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023da0e5097f46df7092d5280b02efb9bbf8d93298daeced42652463e357d636" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e156ed3802e35eefe894ef2671bc6c889303d8a7e110b5e1b48f504b91362f" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ashpd" +version = "0.13.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "getrandom 0.4.3", + "serde", + "serde_repr", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey 0.1.1", + "rayon", + "thiserror 2.0.21", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.8.9", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec 0.9.1", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.13.2", + "block", + "cocoa-foundation 0.2.1", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "collections" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "gpui_util", + "indexmap", + "rustc-hash 2.1.3", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" +dependencies = [ + "bitflags 2.13.2", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 2.1.3", + "self_cell", + "skrifa 0.40.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "cxx" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f6de320895f42e6e081abb5c7983bedcf0b6d0ff9323de0d33f620c8ac1199" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash 0.2.0", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fde53ca86b9704a943fef0f1e1d836239a6aedca5de3c65fa9f97ec0bd46d39" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn 3.0.6", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bae89236c811fd4d08ed3441759ac4ac98d752cbfd3de341315ba16ad20ec3" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49045042e5fced01b80742aba5508de82aa4f13677ed3fa2b4cda709c40c5918" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b181252e2e3d3b5d183afbdc033a3958b445eb3e1a0ae65fc3d4f5259f5da6fd" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "derive_refineable" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.6+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "fontdue" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7894823fa221401399e2598f8b63f81ac77ff5c63248b7656779bff1632d7d3d" +dependencies = [ + "hashbrown 0.15.5", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows 0.62.2", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.2", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "gpui" +version = "0.2.2" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "accesskit", + "anyhow", + "async-channel", + "async-task", + "backtrace", + "bitflags 2.13.2", + "chrono", + "collections", + "ctor", + "derive_more", + "embed-resource", + "etagere", + "futures", + "futures-concurrency", + "getrandom 0.3.4", + "gpui_macros", + "gpui_shared_string", + "gpui_util", + "hdrhistogram", + "heapless", + "http_client", + "image", + "inventory", + "itertools", + "libc", + "log", + "lyon", + "num_cpus", + "objc2-core-foundation", + "objc2-core-video", + "parking", + "parking_lot", + "pin-project", + "pollster 0.4.0", + "postage", + "profiling", + "proptest", + "rand 0.9.5", + "raw-window-handle", + "refineable", + "regex", + "resvg", + "scheduler", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.1", + "strum", + "sum_tree", + "taffy", + "thiserror 2.0.21", + "ttf-parser", + "url", + "usvg", + "uuid", + "waker-fn", + "web-time", + "windows 0.62.2", + "zed-scap", +] + +[[package]] +name = "gpui_apple" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "anyhow", + "async-task", + "block2 0.6.2", + "cbindgen", + "collections", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "derive_more", + "dispatch2", + "etagere", + "foreign-types", + "futures", + "gpui", + "gpui_util", + "image", + "libc", + "log", + "mach2", + "metal", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-security", + "parking_lot", + "pathfinder_geometry", + "smallvec", + "thiserror 2.0.21", + "zed-font-kit", +] + +[[package]] +name = "gpui_linux" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "accesskit", + "accesskit_unix", + "anyhow", + "as-raw-xcb-connection", + "ashpd", + "bytemuck", + "calloop", + "calloop-wayland-source", + "collections", + "filedescriptor", + "futures", + "gpui", + "gpui_util", + "gpui_wgpu", + "http_client", + "image", + "libc", + "log", + "notify-rust", + "oo7", + "open", + "parking_lot", + "raw-window-handle", + "smallvec", + "smol", + "strum", + "url", + "uuid", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "x11rb", + "xkbcommon", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui_macos" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "accesskit", + "accesskit_macos", + "anyhow", + "block2 0.6.2", + "cocoa 0.26.0", + "collections", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "ctor", + "dispatch2", + "foreign-types", + "futures", + "gpui", + "gpui_apple", + "gpui_util", + "image", + "itertools", + "log", + "objc", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-user-notifications", + "parking_lot", + "raw-window-handle", + "scheduler", + "semver", + "smallvec", + "strum", + "uuid", +] + +[[package]] +name = "gpui_macros" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui_platform" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "gpui", + "gpui_linux", + "gpui_macos", + "gpui_web", + "gpui_wgpu", + "gpui_windows", +] + +[[package]] +name = "gpui_shared_string" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "schemars", + "serde", + "smol_str", +] + +[[package]] +name = "gpui_util" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "anyhow", + "log", + "which", +] + +[[package]] +name = "gpui_web" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "anyhow", + "async-channel", + "console_error_panic_hook", + "futures", + "gpui", + "gpui_wgpu", + "http_client", + "js-sys", + "log", + "parking_lot", + "raw-window-handle", + "scheduler", + "smallvec", + "unicode-properties", + "unicode-script", + "unicode-segmentation", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_thread", + "web-sys", + "web-time", +] + +[[package]] +name = "gpui_wgpu" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "anyhow", + "ash", + "bytemuck", + "collections", + "cosmic-text", + "etagere", + "gpui", + "gpui_util", + "image", + "itertools", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "swash", + "unicode-bidi", + "unicode-segmentation", + "web-sys", + "wgpu", + "zed-font-kit", +] + +[[package]] +name = "gpui_windows" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "accesskit", + "accesskit_windows", + "anyhow", + "collections", + "dunce", + "etagere", + "futures", + "gpui", + "gpui_util", + "image", + "itertools", + "log", + "parking_lot", + "rand 0.9.5", + "raw-window-handle", + "scheduler", + "smallvec", + "uuid", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-numerics 0.3.1", + "windows-registry", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hdrhistogram" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d1053f4708f0af3cf9fc5bffc7e68a914a3c45becb231c80068c9c3f78bea" +dependencies = [ + "byteorder", + "num-traits", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http_client" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "anyhow", + "bytes", + "futures", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error 2.0.1", +] + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.2", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.13.2", + "serde", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "ksni" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "814b44c24cd2cb236c3b8a41c7f08237b452a8e76ecaa81f1cec40b5b678215b" +dependencies = [ + "async-executor", + "async-io", + "async-lock", + "futures-channel", + "futures-lite", + "futures-util", + "pastey 0.2.3", + "serde", + "task-local", + "zbus", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +dependencies = [ + "serde_core", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdfa8785f95e57914ddb35e3b59994aeba6f5e79e9cfd03da1c269f010f36009" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "time", + "uuid", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types 0.7.0", + "libxdo", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "once_cell", + "png 0.17.16", + "thiserror 1.0.69", + "windows-sys 0.59.0", +] + +[[package]] +name = "muda" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4340e89c178916e62879c5c36b7f699019369be413e496178a6983ce91b8bc03" +dependencies = [ + "crossbeam-channel", + "dpi", + "keyboard-types 0.8.3", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "png 0.18.1", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "naga" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "notify-rust" +version = "4.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe430c879247371c47fdedd328ad4e301e6b30b533c5fd5db1c06d75253d0e46" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9a86e097b0d187ad0e65667c2f58b9254671e86e7dbb78036b16692eae099" +dependencies = [ + "libm", + "num-integer", + "num-iter", + "num-traits", + "once_cell", + "rand 0.9.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oo7" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" +dependencies = [ + "aes", + "ashpd", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest", + "endi", + "futures-lite", + "futures-util", + "getrandom 0.4.3", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "serde", + "serde_bytes", + "sha2", + "subtle", + "zbus", + "zbus_macros", + "zeroize", + "zvariant", +] + +[[package]] +name = "open" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +dependencies = [ + "is-wsl", + "libc", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "parking_lot", + "pin-project", + "pollster 0.2.5", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.15+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "proptest" +version = "1.10.0" +source = "git+https://github.com/proptest-rs/proptest?rev=3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b#3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.13.2", + "num-traits", + "proptest-macro", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-macro" +version = "0.5.0" +source = "git+https://github.com/proptest-rs/proptest?rev=3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b#3dca198a8fef1b32e3a66f1e1897c955b4dc5b5b" +dependencies = [ + "convert_case 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.5", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.21", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "react-native-gpui" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-channel", + "base64", + "cocoa 0.25.0", + "cxx", + "env_logger", + "fontdue", + "futures", + "gpui", + "gpui_platform", + "http_client", + "image", + "log", + "muda 0.15.3", + "objc", + "raw-window-handle", + "rn-gpui-module", + "rn-gpui-module-build", + "rn-gpui-sys", + "serde_json", + "tray-icon", + "tungstenite", + "windows-sys 0.59.0", + "x11-dl", + "zbus", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.5", + "once_cell", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "refineable" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "derive_refineable", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "rn-gpui-module" +version = "0.1.0" +dependencies = [ + "futures", +] + +[[package]] +name = "rn-gpui-module-build" +version = "0.1.0" + +[[package]] +name = "rn-gpui-module-generated-test" +version = "0.1.0" +dependencies = [ + "futures", + "rn-gpui-module", + "rn-gpui-module-build", +] + +[[package]] +name = "rn-gpui-sys" +version = "0.1.0" +dependencies = [ + "cmake", + "cxx", + "cxx-build", + "log", + "rn-gpui-module", + "serde_json", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scheduler" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.5", + "wasm_thread", + "web-time", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.6", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "serde_core", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "sum_tree" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "heapless", + "rayon", + "tracing", + "ztracing", +] + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.23", + "version-compare", +] + +[[package]] +name = "taffy" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "639627c87f43b9181c811f40a6296409e093a17bc761214cba3c15df74f86b99" +dependencies = [ + "arrayvec", + "serde", + "slotmap", + "smallvec", +] + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "task-local" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2972044a9e5e448a506a7ff6f0d03b566d8ef4cd6918a58fc59835a0f8666626" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36" +dependencies = [ + "thiserror 2.0.21", + "windows 0.62.2", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5cc5858ecf9574ddd8f0032a2ff7ffd6f7724d2b81e48b9374984b3fd6ddc9" +dependencies = [ + "crossbeam-channel", + "ksni", + "muda 0.21.2", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.18.1", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.5", + "sha1", + "thiserror 2.0.21", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree 0.21.1", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm_thread" +version = "0.3.3" +source = "git+https://github.com/zed-industries/wasm_thread?rev=0cf96c7708dfb97ccf3da50347e25edcf75d6937#0cf96c7708dfb97ccf3da50347e25edcf75d6937" +dependencies = [ + "futures", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "log", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bit-vec 0.9.1", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.9.1", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.21", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-result 0.4.1", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-capture" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" +dependencies = [ + "parking_lot", + "rayon", + "thiserror 2.0.21", + "windows 0.61.3", + "windows-future 0.2.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcb" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566" +dependencies = [ + "bitflags 2.13.2", + "libc", + "quick-xml", + "x11", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.1" +source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow 1.0.4", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "git+https://github.com/zed-industries/font-kit?rev=94b0f28166665e8fd2f53ff6d268a14955c82269#94b0f28166665e8fd2f53ff6d268a14955c82269" +dependencies = [ + "bitflags 2.13.2", + "byteorder", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "core-text", + "dirs", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "git+https://github.com/zed-industries/scap?rev=4afea48c3b002197176fb19cd0f9b180dd36eaac#4afea48c3b002197176fb19cd0f9b180dd36eaac" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.8", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "ztracing" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" +dependencies = [ + "tracing", + "ztracing_macro", +] + +[[package]] +name = "ztracing_macro" +version = "0.1.0" +source = "git+https://github.com/streetwriters/gpui-fast?rev=8da7d89#8da7d899c7ddb78ad718375ca118deac373eda3a" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "serde_bytes", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow 1.0.4", +] diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/Cargo.toml b/vendor/react-native-gpui/packages/core/ReactGpui/Cargo.toml new file mode 100644 index 0000000000..ccb57c453a --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/Cargo.toml @@ -0,0 +1,57 @@ +[workspace] +resolver = "2" +members = [ + "rn-gpui-sys", + "rn-gpui", + "rn-gpui-module", + "rn-gpui-module-build", + "rn-gpui-module/tests/generated", +] + +# Set CARGO_PROFILE_DEV_DEBUG=full to opt into full debug information. +[profile.dev] +debug = "line-tables-only" +split-debuginfo = "packed" + +# The same dev profile as every other workspace root in the checkout (the tester's +# and each module's): they all build into the one root `target/`, and a profile +# that differs gives the same dependency a different hash, so it is compiled and +# stored once per variant. +[profile.dev.package."*"] +debug = false +opt-level = 3 + +[profile.dev.package.rn-gpui-sys] +opt-level = 0 + +[profile.release] +# Line tables, not `false`: a release crash is worth symbolizing, and with +# `split-debuginfo` the debug info lives in a separate dSYM rather than the +# binary, so it costs the package nothing. +debug = "line-tables-only" +# Strip the symbol table: nothing reads it at run time. +## Keep the symbols Cargo's link step produces: `strip = "symbols"` runs in the +## linker, so `dsymutil` runs afterwards on an already-stripped binary and writes +## an empty dSYM. The stripping happens in `package-desktop` instead, after the +## dSYM exists. +strip = "none" +split-debuginfo = "packed" +# Full LTO: measured on the tester, 27.3 MB with thin and 25.3 MB with fat, at +# 112 s and 161 s for a clean build. 1.4x the time for 2 MB less is worth it. +lto = "fat" +codegen-units = 1 + +[workspace.dependencies] +gpui = { git = "https://github.com/streetwriters/gpui-fast", rev = "8da7d89" } +gpui_platform = { git = "https://github.com/streetwriters/gpui-fast", rev = "8da7d89" } +cxx = "=1.0.202" +cxx-build = "=1.0.202" +cmake = "0.1" +async-channel = "2" +anyhow = "1" +log = "0.4" +env_logger = "0.11" +serde_json = "1" +futures = "0.3" +http_client = { git = "https://github.com/streetwriters/gpui-fast", rev = "8da7d89" } +fontdue = "0.9" diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/GPUI_PIN.md b/vendor/react-native-gpui/packages/core/ReactGpui/GPUI_PIN.md new file mode 100644 index 0000000000..4d4ef160a8 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/GPUI_PIN.md @@ -0,0 +1,202 @@ +# GPUI pin + +We build on the `streetwriters/gpui-fast` fork: GPUI extracted from the Zed +monorepo, with a retained-mode layout engine (unchanged views are replayed, +layout nodes reused). It keeps upstream GPUI's API; fork code lives in `fast/` +dirs with small hooks. Experimental, git-only (no crates.io releases or tags), +so we pin by commit SHA and bump on purpose. + +## Pinned + +- Repository: https://github.com/streetwriters/gpui-fast +- Branch: `rn-gpui-linux` (tracks upstream `main`; Linux fixes are commits on + top so each one can become an upstream pull request) +- Commit: `8da7d89` (`rn-gpui-linux`, 2026-10-23: `7bf381d` plus the + rounded content masks below) +- Pinned in: `Cargo.toml` `[workspace.dependencies] gpui.rev` + +## Startup deferrals + +- `c380367` `gpui_macos: defer work the first frame does not read` + - `MacKeyboardLayout::new` runs on first key use rather than at platform + creation. It cost about 30 ms — `platform created` fell from 43 ms to 12 ms + once it was lazy, and nothing in the first frame reads a keyboard layout. + - `NSWindow.setAllowsAutomaticWindowTabbing(false)` is set once at platform + start instead of per window, so `NSWindow.tabGroup` is nil from the outset + and `tab_bar_visible` answers without asking AppKit. A window that asks for + a tabbing identifier re-enables it in its own initialiser. + +## Carried patches on top of upstream + +- `7338a98` `gpui: map X11 windows only when shown` (honour `show: false`) +- `2895a8f` `gpui_linux: apply resizes to unmapped X11 windows` +- `09a45a7` `gpui_linux: log a missing XInput pointer as a warning` +- `8034d63` `gpui_linux: implement render_to_image for X11 and Wayland` +- `f8edd01` `gpui: expose accessibility identifiers and states in the debug tree` +- `f1abf86` `gpui_linux: track the X11 map state from map and unmap events` +- `1db8564` `gpui_windows: deliver hover callbacks outside the window procedure` + — the commit message says "hover and resize", but only hover is deferred in it: + `WM_SIZE` and `WM_MOVE` still invoked their callbacks inline, so the earlier + pin compensated in shared `window.rs` instead. `172d79f` finishes the job in + the backend and that compensation is gone. +- `8e846d2` `gpui_windows: skip windows that are already gone` +- `ebf3ced` `gpui: ignore platform callbacks for a window that has been removed` + — amended in effect by `394557f`: that commit made five callbacks discard their + `update` result, which also silenced a failed `App` borrow. The removed-window + case is now a typed `WindowRemoved` error that only that case ignores; every + other failure is logged again. +- `172d79f` `gpui_windows: defer resize and move out of the window procedure` +- `394557f` `gpui: log a failed borrow in the platform callbacks again` +- `ffc9655` `gpui: defer the move callback so a synchronous move cannot re-enter +App` — a move performed from inside an `App` update (a JS `set_position`, + which AppKit implements with `setFrameTopLeftPoint:`) is notified before that + call returns, so `on_moved` ran `handle.update` while the caller still held + the `App` and logged `ERROR gpui::window] RefCell already borrowed`. It now + defers through the foreground executor, as `on_appearance_changed` already + does. macOS-only in practice, but the callback is shared, so Windows and Linux + re-run their window tests against this pin. + +### Carried patch: `on_hit_test_window_control` order + +- `a72b326` `gpui: answer the control hit-test for the frontmost element` + + The callback answered with the first entry of `window_control_hitboxes` under + the pointer. That list is in paint order, and an element inserts its own + control hitbox before its children paint, so an ancestor always won — a drag + region covering a title bar matched before every button inside it. On Windows + the whole bar therefore answered `HTCAPTION`, and a press on a caption button + or any interactive child became a non-client message: the window started + moving instead of the button receiving a mouse-down. + + It now walks the real hit test front to back, the order pointer dispatch uses. + A frontmost hitbox that is not a control area returns `None` rather than + continuing the walk, because every hitbox in that list belongs to an element + that takes the mouse, focus, scroll or a cursor, so it owns the press. + + It was fixed in the fork rather than worked around in `rn-gpui` because the + alternative — not marking the drag region at all on Windows — would lose + `HTCAPTION` and with it snap layouts, double-click maximise and the system + move loop (`start_window_move()` is a no-op in `gpui_windows`). + +### Carried patch: every window callback ignores a removed window + +- `40700ec` `gpui: ignore a window removed before a platform callback arrives` + + `ebf3ced` routed five platform callbacks through `ignore_if_window_removed`; + the rest (`on_resize`, `on_moved`, `on_visual_viewport_changed`, + `on_insets_changed`, `on_appearance_changed`, `on_button_layout_changed`, the + frame request and the input callbacks) still ended in `.log_err()`. A macOS + popup that was dismissed could receive a resize or move after it was removed, + and the e2e `opening and closing a popup repeatedly logs no errors` caught + `ERROR gpui::window] window not found` on CI. Every + `handle.update(...).log_err()` in `Window::new` now goes through the same + helper, so only the removed-window case is ignored and any other failure is + still logged. + +### Carried patch: no error for a cursor message to a closed window + +- `7bf381d` `gpui_windows: do not log a cursor message posted to a window that just closed` + + `set_cursor_style` posts a message to every tracked window, and a window that + has just been destroyed is still in the list for a moment. `PostMessageW` then + fails with `ERROR_INVALID_WINDOW_HANDLE`, which was logged at ERROR and failed + the e2e `a second window opens with its own surface and closes again` on + Windows CI. That one code is now ignored; every other failure is still logged. + +### Carried patch: hidden overflow clipped to rounded corners + +- `8da7d89` `gpui: clip hidden overflow to rounded corners` + + A content mask was a rectangle, so `overflow: hidden` with a border radius + clipped children square, where iOS and Android clip them to the rounded + corners. The mask now also carries one rounded rectangle, which every + renderer (Metal, Direct3D, wgpu) applies per fragment to quads, shadows, + underlines, glyphs, images and paths; scroll layers keep it when they are + composited. Elements opt in with `overflow_clips_corners()`, which `rn-gpui` + sets wherever React Native clips (`overflow: hidden` and scroll views); other + GPUI code keeps a rectangular clip. + + The patch adds API: `ContentMask` has two more fields, so a struct literal + must become `ContentMask::new(bounds)`, and `overflow_clips_corners` is new. + Surfaces and external textures stay rectangular, and nested rounded clips + keep only the innermost one. + +> **Cargo does not notice edits to a git checkout**, which matters only if you +> edit one by hand without bumping the pin. It fingerprints the checkout by +> revision, so after editing `window.rs` it still reports `Fresh gpui` and links +> the old rlib — the change looks like it does nothing. Verified by injecting a +> `compile_error!` and watching the build succeed anyway. If you ever do this, +> `rm -rf target/debug/.fingerprint/gpui-*` and look for a +> `Compiling gpui v0.2.2 (…?rev=…)` line as proof it took. + +`on_hit_test_window_control` used to answer with the first entry of +`rendered_frame.window_control_hitboxes` whose hitbox was under the pointer. +That list is in _paint_ order, and an ancestor paints its own control hitbox +before its children paint theirs, so a drag region covering a title bar always +matched before a button inside it. On Windows the whole bar therefore answered +`HTCAPTION`: a press on a button in the bar moved the window instead of pressing +the button. + +The callback now walks `rendered_frame.hit_test(mouse_position).ids` — front to +back, the same order pointer dispatch uses — and returns the first hitbox's +control area, or `None` if the frontmost hitbox belongs to an element that is not +a control area. Only hitboxes whose elements listen for the mouse, take focus, +scroll or set a cursor are inserted at all, so "frontmost is not a control area" +means "an interactive element owns this press", which is exactly the case where +the window must not be moved. The hit test is computed inside the callback rather +than read from `mouse_hit_test`, because that field is only refreshed when a mouse +event is dispatched and the first `WM_NCHITTEST` arrives before any pointer event. + +To reproduce it on a fresh checkout, apply this to +`crates/gpui/src/window.rs` and remember that **cargo does not notice edits to a +git checkout on its own** — `cargo clean -p gpui` (or a full rebuild) is needed +or the old rlib is reused silently. + +## Upstream (from the repo's `UPSTREAM` file at that commit) + +- Zed repository: https://github.com/zed-industries/zed +- Zed commit: `a1b71072e5b43faef437b471e988fbb5f972c99c` +- gpui-fast import commit holding that import unchanged: + `8848cdcf22eb7479a07a17d8ca627dc8144e4f38` (branch `sync/zed-a1b7107`, + merged as gpui-fast#38) + +`git show :` in gpui-fast gives upstream's version of a +tracked path without a Zed checkout. + +## Policy + +- `rn-gpui` uses only upstream GPUI APIs. gpui-fast promises that upstream code + compiles against its fork unchanged. +- Until gpui-fast lands window composition (zed#62379), carry that patch on our + own branch of gpui-fast if we need it (design doc §5). +- Bump the SHA deliberately; record the new SHA + UPSTREAM Zed commit here. + +## Known limitation: retained views under accessibility + +The retained-view fast path is disabled while a screen reader is active: + +- `crates/gpui/src/fast/retained.rs:544` (`self.a11y.is_active()`) +- `crates/gpui/src/fast/splice.rs:297` (`self.a11y.is_active()`) +- `crates/gpui/src/fast/layers/mod.rs:121` (`!window.a11y.is_active()`) + +With it disabled, a visible window rebuilds its element tree on every refresh, +so the `retained static siblings do not render with changing siblings` e2e check +expects 5 recorded paints but sees every subsequent paint as well. Hidden +(offscreen) windows do not refresh from events, which is why the offscreen e2e +runs pass with the accessibility bus on. + +Because of that, the Linux e2e runner leaves the AT-SPI bus off for **headed** +runs (`packages/rn-gpui-tester/scripts/run-e2e.mjs`), so the native +accessibility assertions skip there. Offscreen runs still enable the bus. +Setting `RN_GPUI_TEST_NATIVE_A11Y=1` forces the bus on even when headed. + +Windows is not affected, and needs no equivalent rule. Activation there is +per process: `gpui_windows` only starts publishing when a client asks the window +for its accessibility provider (`WM_GETOBJECT`), and the test driver asks on +behalf of the assertions, so only the process running an accessibility test has +`a11y.is_active()` set. A headed run was measured with accessibility active and +the `retained static siblings` check passed; each test launches its own app +process, so the activation cannot leak between them. + +Fixing this properly means replaying accessibility subtree updates for retained +views; that is out of scope for now. diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module-build/Cargo.toml b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module-build/Cargo.toml new file mode 100644 index 0000000000..179113aca8 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module-build/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rn-gpui-module-build" +version = "0.1.0" +edition = "2021" +description = "Build script helper that generates Rust TurboModule glue from TypeScript specs" + +[lib] +path = "src/lib.rs" diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module-build/src/lib.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module-build/src/lib.rs new file mode 100644 index 0000000000..44927794d6 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module-build/src/lib.rs @@ -0,0 +1,165 @@ +//! Runs the Rust TurboModule generator for a crate's TypeScript specs. +//! +//! ```no_run +//! fn main() { +//! rn_gpui_module_build::generate(&["../src/NativeFixture.ts"]); +//! } +//! ``` + +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Generates `$OUT_DIR/rn_gpui_specs.rs` from `specs`, relative to the crate's +/// manifest directory, and rebuilds when a spec or the generator changes. +pub fn generate(specs: &[&str]) { + let manifest_dir = PathBuf::from( + std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo"), + ); + let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo")); + let generator = generator_script(&manifest_dir); + println!("cargo:rerun-if-changed={}", generator.display()); + let mut paths = Vec::with_capacity(specs.len()); + for spec in specs { + let path = manifest_dir.join(spec); + if !path.exists() { + panic!( + "rn-gpui-module-build: no spec file at {} (specs are relative to {})", + path.display(), + manifest_dir.display() + ); + } + println!("cargo:rerun-if-changed={}", path.display()); + paths.push(path); + } + let output = out_dir.join("rn_gpui_specs.rs"); + // With no specs the generator would be invoked as `node generator `, + // and it requires at least one spec plus the output, so it exits 2 with its + // usage line. A template app declares no modules, so this is the common + // case for a fresh project: write the empty module list instead of running + // the generator at all. + if paths.is_empty() { + std::fs::write( + &output, + "// Generated by rn-gpui-module-build: no specs were declared.\n", + ) + .unwrap_or_else(|error| { + panic!( + "rn-gpui-module-build: could not write {}: {error}", + output.display() + ) + }); + return; + } + let mut command = Command::new("node"); + command.arg(&generator).args(&paths).arg(&output); + let result = command + .output() + .unwrap_or_else(|error| panic!("rn-gpui-module-build: could not run Node.js: {error}")); + if !result.status.success() { + panic!( + "rn-gpui-module-build: {} failed with {}\nstderr:\n{}\nNode.js and @react-native-gpui/codegen must be installed in the app.", + generator.display(), + result.status, + String::from_utf8_lossy(&result.stderr).trim_end() + ); + } +} + +/// Resolves the generator from the app, so a library inside an app finds the +/// app's copy. +fn generator_script(manifest_dir: &Path) -> PathBuf { + let script = "process.stdout.write(require.resolve('@react-native-gpui/codegen/generate-rust-spec', {paths: [process.argv[1]]}))"; + let result = Command::new("node") + .arg("-e") + .arg(script) + .arg(manifest_dir) + .output() + .unwrap_or_else(|error| panic!("rn-gpui-module-build: could not run Node.js: {error}")); + if !result.status.success() || result.stdout.is_empty() { + panic!( + "rn-gpui-module-build: @react-native-gpui/codegen is not installed for {}\nstderr:\n{}\nAdd it to the app's dependencies.", + manifest_dir.display(), + String::from_utf8_lossy(&result.stderr).trim_end() + ); + } + PathBuf::from(String::from_utf8_lossy(&result.stdout).trim()) +} + +/// Forwards every Node-API symbol the engine exports out of the app's +/// executable, so a stock `.node` addon can resolve them on Windows. +/// +/// Windows has no global symbol lookup: a DLL import names the function *and* +/// the file it comes from. Addons reach Node-API in two ways and both end at the +/// host process's executable — a `node-gyp` addon as a delay-loaded import from +/// `node.exe`, which its `win_delay_load_hook.cc` replaces with the current +/// process image, and a napi-rs addon by probing the host executable first. Our +/// Node-API functions live in the engine's DLL and the executable exports +/// nothing, so each name is re-exported as a forwarder to that DLL. +/// +/// This lives in one place because two build scripts need it — +/// `rn-gpui-tester/desktop/build.rs` and the app template's — and a module +/// definition that disagrees between them is an addon that loads in one app and +/// not the other. +/// +/// Does nothing unless the target is Windows/MSVC and the engine supplied a +/// list. A Windows GNU target gets a warning instead: MinGW's `.def` handling +/// and its `--enable-auto-import` are not what an MSVC-built addon expects, and +/// a silently unforwarded binary is worse than a warning. +pub fn forward_node_api_exports() { + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") { + return; + } + let list = std::env::var("DEP_RN_GPUI_RUNTIME_NODE_API_EXPORTS") + .ok() + .filter(|value| !value.is_empty()); + if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() != Ok("msvc") { + if list.is_some() { + println!( + "cargo:warning=Node-API addons on Windows need the MSVC toolchain; \ + the engine's exports are not forwarded for this target" + ); + } + return; + } + let Some(list) = list else { + // No engine list: either an engine without Node-API or a build that + // never resolved one. Not an error, so a plain app still links. + return; + }; + let library = std::env::var("DEP_RN_GPUI_RUNTIME_NODE_API_LIBRARY") + .ok() + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| { + panic!( + "rn-gpui-module-build: the engine exports a Node-API list without a library name" + ) + }); + + let names = std::fs::read_to_string(&list) + .unwrap_or_else(|error| panic!("rn-gpui-module-build: read {list}: {error}")); + // The list is one name per line; a blank line or a stray comment must not + // become an export (a malformed `.def` fails the link with a message that + // does not name the file). + let names: Vec<&str> = names + .lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with(';')) + .collect(); + if names.is_empty() { + panic!("rn-gpui-module-build: the Node-API export list {list} is empty"); + } + + let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo")); + let def = out_dir.join("node_api_exports.def"); + let mut text = String::from("EXPORTS\n"); + for name in &names { + // A forwarder: the executable exports `name` and the loader resolves it + // from the named DLL. No `@ordinal`, so the name stays usable. + text.push_str(&format!(" {name}={library}.{name}\n")); + } + std::fs::write(&def, text) + .unwrap_or_else(|error| panic!("rn-gpui-module-build: write {}: {error}", def.display())); + + println!("cargo:rerun-if-changed={list}"); + println!("cargo:rustc-link-arg-bins=/DEF:{}", def.display()); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/Cargo.toml b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/Cargo.toml new file mode 100644 index 0000000000..91b3b802eb --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "rn-gpui-module" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures = { workspace = true } diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/error.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/error.rs new file mode 100644 index 0000000000..30d9664a68 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/error.rs @@ -0,0 +1,45 @@ +use std::fmt; + +/// A rejected call: the message reaches JS as the error's message, the code as +/// its `code` property. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ModuleError { + pub code: Option, + pub message: String, +} + +impl ModuleError { + pub fn new(message: impl Into) -> Self { + Self { + code: None, + message: message.into(), + } + } + + pub fn with_code(code: impl Into, message: impl Into) -> Self { + Self { + code: Some(code.into()), + message: message.into(), + } + } +} + +impl fmt::Display for ModuleError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for ModuleError {} + +impl From for ModuleError { + fn from(message: String) -> Self { + Self::new(message) + } +} + +impl From<&str> for ModuleError { + fn from(message: &str) -> Self { + Self::new(message) + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/host.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/host.rs new file mode 100644 index 0000000000..c376809c91 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/host.rs @@ -0,0 +1,208 @@ +use std::any::Any; +use std::sync::{Mutex, OnceLock}; + +use crate::{ModuleError, Value}; + +/// A JS function passed as an argument, which appears as `Value::Callback` and +/// never in a result. Calling it consumes it, so it can be called at most once; +/// dropping it without calling releases the JS function. +pub struct RawCallback { + id: u64, +} + +impl RawCallback { + pub fn new(id: u64) -> Self { + Self { id } + } + + pub fn id(&self) -> u64 { + self.id + } + + pub fn invoke(self, args: Vec) { + let this = std::mem::ManuallyDrop::new(self); + (hooks().invoke_callback)(this.id, args); + } +} + +impl std::fmt::Debug for RawCallback { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_tuple("RawCallback") + .field(&self.id) + .finish() + } +} + +impl Drop for RawCallback { + fn drop(&mut self) { + (hooks().drop_callback)(self.id); + } +} + +/// Emits one module instance's typed and device events. Cheap to clone and +/// usable from any thread; emitting before JS subscribes, or after a reload, +/// does nothing. +#[derive(Clone, Debug)] +pub struct EventEmitter { + module: u64, +} + +impl EventEmitter { + pub fn new(module: u64) -> Self { + Self { module } + } + + pub fn emit(&self, event: &str, value: Option) { + (hooks().emit_event)(self.module, event, value); + } + + pub fn send_event(&self, name: &str, body: Value) { + (hooks().send_event)(self.module, name, body); + } +} + +/// What the host installs so the crate can reach JS; without hooks the calls +/// are recorded in the sink instead. +#[derive(Clone, Copy)] +pub struct HostHooks { + pub invoke_callback: fn(u64, Vec), + pub drop_callback: fn(u64), + pub emit_event: fn(u64, &str, Option), + pub send_event: fn(u64, &str, Value), + /// Reports a failure with nowhere else to go, such as a `void` method that + /// failed on the module thread. + pub log: fn(&str), +} + +static HOST_HOOKS: OnceLock = OnceLock::new(); + +pub fn set_host_hooks(hooks: HostHooks) { + let _ = HOST_HOOKS.set(hooks); +} + +fn hooks() -> HostHooks { + *HOST_HOOKS.get().unwrap_or(&HostHooks { + invoke_callback: sink_invoke_callback, + drop_callback: sink_drop_callback, + emit_event: sink_emit_event, + send_event: sink_send_event, + log: sink_log, + }) +} + +#[derive(Debug)] +pub enum SinkCall { + InvokeCallback { + id: u64, + args: Vec, + }, + DropCallback { + id: u64, + }, + EmitEvent { + module: u64, + event: String, + value: Option, + }, + SendEvent { + module: u64, + name: String, + body: Value, + }, + Log { + message: String, + }, +} + +static SINK: OnceLock>> = OnceLock::new(); + +fn sink() -> &'static Mutex> { + SINK.get_or_init(|| Mutex::new(Vec::new())) +} + +pub fn sink_calls() -> Vec { + let mut calls = sink() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + std::mem::take(&mut *calls) +} + +fn sink_invoke_callback(id: u64, args: Vec) { + sink() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(SinkCall::InvokeCallback { id, args }); +} + +fn sink_drop_callback(id: u64) { + sink() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(SinkCall::DropCallback { id }); +} + +fn sink_emit_event(module: u64, event: &str, value: Option) { + sink() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(SinkCall::EmitEvent { + module, + event: event.to_string(), + value, + }); +} + +fn sink_send_event(module: u64, name: &str, body: Value) { + sink() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(SinkCall::SendEvent { + module, + name: name.to_string(), + body, + }); +} + +fn sink_log(message: &str) { + sink() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(SinkCall::Log { + message: message.to_string(), + }); +} + +pub fn log_error(location: &str, error: &ModuleError) { + (hooks().log)(&format!("{location}: {error}")); +} + +/// Runs a queued `void` call, turning a panic or an error into a log line. +pub fn run_void(location: &str, body: impl FnOnce()) { + if let Err(error) = catch(body) { + log_error(location, &error); + } +} + +/// Turns a caught panic into an error, so it never unwinds into C++ or kills a +/// module thread. +pub fn panic_error(payload: Box) -> ModuleError { + let message = payload + .downcast_ref::<&str>() + .map(|text| (*text).to_string()) + .or_else(|| payload.downcast_ref::().cloned()) + .unwrap_or_else(|| "panicked".to_string()); + ModuleError::new(format!("the module panicked: {message}")) +} + +pub fn catch(body: impl FnOnce() -> T) -> Result { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)).map_err(panic_error) +} + +pub async fn catch_async(future: F) -> Result { + use futures::FutureExt; + std::panic::AssertUnwindSafe(future) + .catch_unwind() + .await + .map_err(panic_error) +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/lib.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/lib.rs new file mode 100644 index 0000000000..9309c80006 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/lib.rs @@ -0,0 +1,44 @@ +//! The runtime a Rust TurboModule is written against: the value tree, errors, +//! events and threads the generated glue uses. It depends on nothing but +//! `futures`, so it needs no C++ toolchain and no JS runtime to test. + +mod error; +mod host; +mod private; +mod queue; +mod registry; +mod value; + +pub use error::ModuleError; +pub use host::{ + log_error, run_void, set_host_hooks, sink_calls, EventEmitter, HostHooks, RawCallback, SinkCall, +}; +pub use private::Path; +pub use queue::{ + is_main_thread, note_main_thread, set_main_spawner, spawn_main, Job, LocalBoxFuture, + ModuleQueue, +}; +pub use registry::{ + descriptor, descriptors, register, EventInfo, MethodInfo, MethodKind, ModuleDescriptor, + ModuleInstance, ModuleThread, +}; +pub use value::{ArrayBuffer, FromValue, IntoValue, Value}; + +#[doc(hidden)] +pub mod __private { + pub use crate::private::{ + catch, catch_async, field, location, log_error, panic_error, run_void, Args, FieldMap, Path, + }; +} + +/// Includes what `rn-gpui-module-build` generated from the crate's specs. +#[macro_export] +macro_rules! include_specs { + () => { + #[allow(clippy::all, unused)] + pub mod rn_gpui_specs { + include!(concat!(env!("OUT_DIR"), "/rn_gpui_specs.rs")); + } + pub use rn_gpui_specs::*; + }; +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/private.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/private.rs new file mode 100644 index 0000000000..a51e5a6743 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/private.rs @@ -0,0 +1,119 @@ +//! Helpers the generated glue calls; anything here can change with the +//! generator. +#![doc(hidden)] + +use crate::{FromValue, ModuleError, Value}; + +pub use crate::value::Path; + +/// The cursor over one call's arguments, converted on the thread that received +/// them. +pub struct Args { + module: &'static str, + method: &'static str, + values: Vec, + index: usize, +} + +impl Args { + pub fn new(module: &'static str, method: &'static str, values: Vec) -> Self { + Self { + module, + method, + values, + index: 0, + } + } + + pub fn next_callback(&mut self, name: &'static str) -> Result { + let index = self.index; + self.index += 1; + let value = match self.values.get_mut(index) { + Some(slot) => std::mem::replace(slot, Value::Null), + None => { + return Err(ModuleError::new(format!( + "{}.{}: argument {index} ({name}) is missing", + self.module, self.method + ))) + } + }; + let path = Path::argument(self.module, self.method, index, name); + match value { + Value::Callback(callback) => Ok(callback), + other => Err(path.error(format!("expected function, got {}", other.type_name()))), + } + } + + pub fn next(&mut self, name: &'static str) -> Result { + let index = self.index; + self.index += 1; + let value = match self.values.get_mut(index) { + Some(slot) => std::mem::replace(slot, Value::Null), + None => { + return Err(ModuleError::new(format!( + "{}.{}: argument {index} ({name}) is missing", + self.module, self.method + ))) + } + }; + let mut path = Path::argument(self.module, self.method, index, name); + T::from_value(value, &mut path) + } +} + +/// The fields of one object, so a generated struct can take them by name and +/// report a missing or mistyped one. +pub struct FieldMap { + fields: Vec<(String, Value)>, +} + +impl FieldMap { + pub fn new(fields: Vec<(String, Value)>) -> Self { + Self { fields } + } + + pub fn take(&mut self, path: &mut Path, name: &str) -> Result { + match self.remove(name) { + Some(value) => field(path, name, value), + None => { + path.push_field(name); + let error = path.error("missing field"); + path.pop_field(); + Err(error) + } + } + } + + pub fn take_optional( + &mut self, + path: &mut Path, + name: &str, + ) -> Result, ModuleError> { + match self.remove(name) { + Some(Value::Null) => Ok(None), + Some(value) => field(path, name, value).map(Some), + None => Ok(None), + } + } + + fn remove(&mut self, name: &str) -> Option { + let index = self + .fields + .iter() + .position(|(key, _)| key.as_str() == name)?; + Some(self.fields.remove(index).1) + } +} + +pub fn field(path: &mut Path, name: &str, value: Value) -> Result { + path.push_field(name); + let converted = T::from_value(value, path); + path.pop_field(); + converted +} + +pub fn location(module: &'static str, method: &'static str) -> String { + format!("{module}.{method}") +} + +pub use crate::host::{catch, catch_async, log_error, panic_error, run_void}; diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/queue.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/queue.rs new file mode 100644 index 0000000000..c15b171dc4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/queue.rs @@ -0,0 +1,105 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::OnceLock; +use std::thread::JoinHandle; + +use futures::channel::mpsc; +use futures::executor::LocalPool; +use futures::task::LocalSpawnExt; +use futures::StreamExt; + +pub type LocalBoxFuture = Pin + 'static>>; + +/// A job is built on the calling thread and turned into a future on the thread +/// that polls it, so the future itself never crosses threads. +pub type Job = Box LocalBoxFuture<()> + Send + 'static>; + +/// A serial executor for one module: one thread and one `LocalPool`, so a job +/// that awaits does not hold up the next job. +pub struct ModuleQueue { + sender: Option>, + thread: Option>, +} + +impl ModuleQueue { + pub fn new(name: &str) -> Self { + let (sender, mut receiver) = mpsc::unbounded::(); + let thread = std::thread::Builder::new() + .name(format!("rn.module.{name}")) + .spawn(move || { + let mut pool = LocalPool::new(); + let spawner = pool.spawner(); + pool.run_until(async move { + while let Some(job) = receiver.next().await { + let _ = spawner.spawn_local(job()); + } + }); + }) + .expect("spawn a module thread"); + Self { + sender: Some(sender), + thread: Some(thread), + } + } + + pub fn post(&self, job: Job) { + if let Some(sender) = &self.sender { + // The receiver only goes away when the queue is dropped, and + // `post` cannot outlive it. + let _ = sender.unbounded_send(job); + } + } +} + +impl Drop for ModuleQueue { + fn drop(&mut self) { + // Closing the channel ends `run_until`, which drops unfinished futures. + self.sender = None; + let Some(thread) = self.thread.take() else { + return; + }; + if thread.thread().id() == std::thread::current().id() { + // A job dropped the queue that owns this thread; joining would + // deadlock on itself. + return; + } + let _ = thread.join(); + } +} + +static MAIN_THREAD: OnceLock = OnceLock::new(); + +/// Records the calling thread as the GPUI main thread. The host calls this when +/// the main loop starts. +pub fn note_main_thread() { + let _ = MAIN_THREAD.set(std::thread::current().id()); +} + +pub fn is_main_thread() -> bool { + MAIN_THREAD + .get() + .is_some_and(|id| *id == std::thread::current().id()) +} + +static MAIN_SPAWNER: OnceLock> = OnceLock::new(); + +/// Installed once by the host so `ModuleThread::Main` jobs reach the main +/// thread. +pub fn set_main_spawner(spawn: impl Fn(Job) + Send + Sync + 'static) { + let _ = MAIN_SPAWNER.set(Box::new(spawn)); +} + +/// Runs `job` on the main thread, or drops it when no spawner is installed. +pub fn spawn_main(job: Job) { + match MAIN_SPAWNER.get() { + Some(spawn) => spawn(job), + None => { + static WARNED: std::sync::Once = std::sync::Once::new(); + WARNED.call_once(|| { + eprintln!( + "rn_gpui_module::spawn_main: no main-thread spawner installed; job dropped" + ); + }); + } + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/registry.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/registry.rs new file mode 100644 index 0000000000..cb1c762e52 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/registry.rs @@ -0,0 +1,104 @@ +use std::sync::{Arc, Mutex, OnceLock}; + +use crate::{EventEmitter, LocalBoxFuture, ModuleError, Value}; + +/// Where a module's `void` and `Promise` methods run; value methods always run +/// on the JS thread. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ModuleThread { + Own, + Main, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MethodKind { + Value, + Void, + Promise, +} + +pub struct MethodInfo { + pub name: &'static str, + pub arg_count: usize, + /// How many arguments JS has to pass: the parameters up to the last one + /// that is not `Option<_>`, since an optional parameter may be left out. + pub required_args: usize, + pub kind: MethodKind, + /// Whether the spec returns `void`, which makes a promise resolve with + /// `undefined` rather than `null`. + pub returns_void: bool, + /// Positions that hold a callback, which the host replaces with a + /// `RawCallback` id before converting the arguments. + pub callback_args: &'static [usize], +} + +pub struct EventInfo { + pub name: &'static str, + pub has_value: bool, +} + +/// Implemented for the author's type by the generated glue. +pub trait ModuleInstance: Send + Sync + 'static { + fn call_value(&self, method: usize, args: Vec) -> Result; + + /// Converts the arguments and returns the call for the host to run on the + /// module's thread, so a conversion failure reaches JS before anything is + /// queued. + fn prepare_void( + self: Arc, + method: usize, + args: Vec, + ) -> Result, ModuleError>; + + /// Called on the JS thread; the future it returns is polled on the module's + /// thread. + fn call_promise( + self: Arc, + method: usize, + args: Vec, + ) -> Result>, ModuleError>; +} + +pub struct ModuleDescriptor { + pub name: &'static str, + pub thread: ModuleThread, + pub methods: &'static [MethodInfo], + pub events: &'static [EventInfo], + pub create: fn(EventEmitter) -> Arc, +} + +static REGISTRY: OnceLock>> = OnceLock::new(); + +fn registry() -> &'static Mutex> { + REGISTRY.get_or_init(|| Mutex::new(Vec::new())) +} + +/// Registering a duplicate panics, which would otherwise poison the lock and +/// fail every later lookup: the list itself is left unchanged by that panic, so +/// recovering it is safe. +fn modules() -> std::sync::MutexGuard<'static, Vec<&'static ModuleDescriptor>> { + registry() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) +} + +/// Registers a module, panicking on a duplicate name. +pub fn register(descriptor: ModuleDescriptor) { + let descriptor: &'static ModuleDescriptor = Box::leak(Box::new(descriptor)); + let mut modules = modules(); + if modules.iter().any(|module| module.name == descriptor.name) { + panic!( + "a Rust TurboModule named {} is already registered", + descriptor.name + ); + } + modules.push(descriptor); +} + +pub fn descriptor(name: &str) -> Option<&'static ModuleDescriptor> { + modules().iter().find(|module| module.name == name).copied() +} + +pub fn descriptors() -> Vec<&'static ModuleDescriptor> { + modules().clone() +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/value.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/value.rs new file mode 100644 index 0000000000..955f886c5e --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/src/value.rs @@ -0,0 +1,297 @@ +use std::fmt; + +use crate::{ModuleError, RawCallback}; + +/// A JS value detached from the JS runtime. `Callback` is the one member that +/// is not data; it is consumed by calling it. +#[derive(Debug)] +pub enum Value { + Null, + Bool(bool), + Number(f64), + String(String), + /// The bytes of an `ArrayBuffer`. + Buffer(Vec), + Array(Vec), + Object(Vec<(String, Value)>), + Callback(RawCallback), +} + +impl Value { + pub fn object(fields: impl IntoIterator, Value)>) -> Self { + Self::Object( + fields + .into_iter() + .map(|(key, value)| (key.into(), value)) + .collect(), + ) + } + + pub fn type_name(&self) -> &'static str { + match self { + Self::Null => "null", + Self::Bool(_) => "boolean", + Self::Number(_) => "number", + Self::String(_) => "string", + Self::Buffer(_) => "ArrayBuffer", + Self::Array(_) => "array", + Self::Object(_) => "object", + Self::Callback(_) => "function", + } + } +} + +/// The bytes of an `ArrayBuffer`, which the spec's `ArrayBuffer` maps to. A +/// `Vec` cannot: it already converts as an array of numbers. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ArrayBuffer(pub Vec); + +impl ArrayBuffer { + pub fn as_slice(&self) -> &[u8] { + &self.0 + } + + pub fn into_vec(self) -> Vec { + self.0 + } +} + +impl std::ops::Deref for ArrayBuffer { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for ArrayBuffer { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl From for Vec { + fn from(buffer: ArrayBuffer) -> Self { + buffer.0 + } +} + +impl FromValue for ArrayBuffer { + fn from_value(value: Value, path: &mut Path) -> Result { + match value { + Value::Buffer(bytes) => Ok(Self(bytes)), + other => Err(path.error(format!("expected ArrayBuffer, got {}", other.type_name()))), + } + } +} + +impl IntoValue for ArrayBuffer { + fn into_value(self) -> Value { + Value::Buffer(self.0) + } +} + +/// Where a conversion is, for error messages: +/// `NativeFixture.summarize: argument 1 (options).label`. +pub struct Path { + module: &'static str, + method: &'static str, + argument: Option<(&'static str, usize)>, + fields: Vec, +} + +impl Path { + pub fn argument( + module: &'static str, + method: &'static str, + index: usize, + name: &'static str, + ) -> Self { + Self { + module, + method, + argument: Some((name, index)), + fields: Vec::new(), + } + } + + /// For a value a method converts itself, such as JSON a lower layer built. + pub fn result(module: &'static str, method: &'static str) -> Self { + Self { + module, + method, + argument: None, + fields: Vec::new(), + } + } + + pub fn location(&self) -> String { + let mut location = format!("{}.{}", self.module, self.method); + if let Some((name, index)) = self.argument { + location.push_str(&format!(": argument {index} ({name})")); + } + for field in &self.fields { + location.push('.'); + location.push_str(field); + } + location + } + + pub fn error(&self, detail: impl fmt::Display) -> ModuleError { + ModuleError::new(format!("{}: {detail}", self.location())) + } + + pub fn push_field(&mut self, name: impl fmt::Display) { + self.fields.push(name.to_string()); + } + + pub fn pop_field(&mut self) { + self.fields.pop(); + } +} + +/// A Rust type a `Value` can be read as; generated structs and enums implement +/// it field by field. +pub trait FromValue: Sized { + fn from_value(value: Value, path: &mut Path) -> Result; +} + +pub trait IntoValue { + fn into_value(self) -> Value; +} + +impl FromValue for Value { + fn from_value(value: Value, _path: &mut Path) -> Result { + Ok(value) + } +} + +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } +} + +impl FromValue for String { + fn from_value(value: Value, path: &mut Path) -> Result { + match value { + Value::String(text) => Ok(text), + other => Err(path.error(format!("expected string, got {}", other.type_name()))), + } + } +} + +impl IntoValue for String { + fn into_value(self) -> Value { + Value::String(self) + } +} + +impl FromValue for f64 { + fn from_value(value: Value, path: &mut Path) -> Result { + match value { + Value::Number(number) => Ok(number), + other => Err(path.error(format!("expected number, got {}", other.type_name()))), + } + } +} + +impl IntoValue for f64 { + fn into_value(self) -> Value { + Value::Number(self) + } +} + +impl FromValue for i32 { + fn from_value(value: Value, path: &mut Path) -> Result { + match value { + Value::Number(number) + if number.fract() == 0.0 + && number >= f64::from(i32::MIN) + && number <= f64::from(i32::MAX) => + { + Ok(number as i32) + } + Value::Number(number) => Err(path.error(format!("expected Int32, got {number}"))), + other => Err(path.error(format!("expected Int32, got {}", other.type_name()))), + } + } +} + +impl IntoValue for i32 { + fn into_value(self) -> Value { + Value::Number(f64::from(self)) + } +} + +impl FromValue for bool { + fn from_value(value: Value, path: &mut Path) -> Result { + match value { + Value::Bool(flag) => Ok(flag), + other => Err(path.error(format!("expected boolean, got {}", other.type_name()))), + } + } +} + +impl IntoValue for bool { + fn into_value(self) -> Value { + Value::Bool(self) + } +} + +impl FromValue for () { + fn from_value(value: Value, path: &mut Path) -> Result { + match value { + Value::Null => Ok(()), + other => Err(path.error(format!("expected null, got {}", other.type_name()))), + } + } +} + +impl IntoValue for () { + fn into_value(self) -> Value { + Value::Null + } +} + +impl FromValue for Option { + fn from_value(value: Value, path: &mut Path) -> Result { + match value { + Value::Null => Ok(None), + value => T::from_value(value, path).map(Some), + } + } +} + +impl IntoValue for Option { + fn into_value(self) -> Value { + match self { + Some(value) => value.into_value(), + None => Value::Null, + } + } +} + +impl FromValue for Vec { + fn from_value(value: Value, path: &mut Path) -> Result { + match value { + Value::Array(items) => items + .into_iter() + .enumerate() + .map(|(index, item)| { + path.push_field(index); + let converted = T::from_value(item, path); + path.pop_field(); + converted + }) + .collect(), + other => Err(path.error(format!("expected array, got {}", other.type_name()))), + } + } +} + +impl IntoValue for Vec { + fn into_value(self) -> Value { + Value::Array(self.into_iter().map(IntoValue::into_value).collect()) + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/Cargo.toml b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/Cargo.toml new file mode 100644 index 0000000000..1afa17c247 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rn-gpui-module-generated-test" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies] +rn-gpui-module = { path = "../.." } +futures = { workspace = true } + +[build-dependencies] +rn-gpui-module-build = { path = "../../../rn-gpui-module-build" } diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/build.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/build.rs new file mode 100644 index 0000000000..75fff811bf --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/build.rs @@ -0,0 +1,3 @@ +fn main() { + rn_gpui_module_build::generate(&["specs/NativeProbe.ts"]); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/specs/NativeProbe.ts b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/specs/NativeProbe.ts new file mode 100644 index 0000000000..fb7315777d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/specs/NativeProbe.ts @@ -0,0 +1,62 @@ +import type { CodegenTypes, TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export type Mode = 'left' | 'right'; + +export type Meta = { + name: string; + extra?: Object; + list?: Array; +}; + +export type Options = { + type: string; + label?: string; + count: Int32; + mode: Mode; + ratio?: number | null; + nested: { flag: boolean; names: Array }; +}; + +export enum Level { + Low = 'low', + High = 'high', +} + +export enum Rank { + Low = 0, + High = 1, +} + +export interface Spec extends TurboModule { + scalar(text: string, count: number, whole: Int32, flag: boolean): boolean; + optionalValue(maybe?: string, nullable: string | null): string | null; + lists( + items: Array, + readonlyItems: ReadonlyArray, + ): Array; + options(value: Options, again: Options): Options; + meta(value: Meta): Meta; + level(level: Level): Mode; + rank(value: Rank): Rank; + match(text: string): string; + anything(value: Object): UnsafeObject; + nothing(): void; + inline(value: { x: number; y?: string }): void; + later(text: string): Promise; + done(): Promise; + lookup(key: string, callback: (value: string, found: boolean) => void): void; + lookupLater( + key: string, + callback: (value: string, found: boolean) => void, + ): Promise; + sum(data: ArrayBuffer): number; + reversed(data: ArrayBuffer): ArrayBuffer; + start(steps: number): void; + readonly onProgress: CodegenTypes.EventEmitter; + readonly onDone: CodegenTypes.EventEmitter; + addListener(eventName: string): void; + removeListeners(count: number): void; +} + +export default TurboModuleRegistry.getEnforcing('NativeProbe'); diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/src/lib.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/src/lib.rs new file mode 100644 index 0000000000..4450ad110d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/generated/src/lib.rs @@ -0,0 +1,509 @@ +//! Compiles the generator's output for a spec that uses every type in the +//! mapping table and calls each method through the descriptor. + +rn_gpui_module::include_specs!(); + +use rn_gpui_module::{ArrayBuffer, ModuleError, ModuleThread, Value}; + +pub struct Probe { + events: NativeProbeEvents, + recorded: std::sync::Mutex>, +} + +impl NativeProbeSpec for Probe { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(events: NativeProbeEvents) -> Self { + Self { + events, + recorded: std::sync::Mutex::new(Vec::new()), + } + } + + fn scalar( + &self, + text: String, + count: f64, + whole: i32, + flag: bool, + ) -> Result { + Ok(!text.is_empty() && count > 0.0 && whole == 1 && flag) + } + + fn optional_value( + &self, + maybe: Option, + nullable: Option, + ) -> Result, ModuleError> { + Ok(match (maybe, nullable) { + (Some(maybe), Some(nullable)) => Some(format!("{maybe}/{nullable}")), + (Some(maybe), None) => Some(maybe), + (None, Some(nullable)) => Some(nullable), + (None, None) => None, + }) + } + + fn lists(&self, items: Vec, readonly_items: Vec) -> Result, ModuleError> { + Ok(items + .iter() + .map(|item| item.len() as i32 + readonly_items.len() as i32) + .collect()) + } + + fn options(&self, value: Options, again: Options) -> Result { + Ok(Options { + r#type: value.r#type, + label: value.label.or(again.label), + count: value.count + again.count, + mode: value.mode, + ratio: value.ratio.or(again.ratio), + nested: value.nested, + }) + } + + fn meta(&self, value: Meta) -> Result { + Ok(value) + } + + fn rank(&self, value: Rank) -> Result { + Ok(value) + } + + fn r#match(&self, text: String) -> Result { + Ok(format!("matched {text}")) + } + + fn level(&self, level: Level) -> Result { + Ok(match level { + Level::Low => LevelResult::Left, + Level::High => LevelResult::Right, + }) + } + + fn anything(&self, value: Value) -> Result { + Ok(value) + } + + fn nothing(&self) {} + + fn inline(&self, value: InlineValue) { + self.recorded.lock().unwrap().push(format!( + "inline {} {}", + value.x, + value.y.unwrap_or_default() + )); + } + + async fn later(&self, text: String) -> Result { + Ok(format!("later {text}")) + } + + async fn done(&self) -> Result<(), ModuleError> { + self.events.on_done(); + Ok(()) + } + + fn lookup(&self, key: String, callback: Box) { + callback(format!("value of {key}"), key == "gpui"); + } + + async fn lookup_later( + &self, + key: String, + callback: Box, + ) -> Result { + let found = key == "gpui"; + if found { + callback(format!("later value of {key}"), true); + } + Ok(if found { "called" } else { "dropped" }.to_string()) + } + + fn sum(&self, data: ArrayBuffer) -> Result { + Ok(data.iter().map(|byte| f64::from(*byte)).sum()) + } + + fn reversed(&self, data: ArrayBuffer) -> Result { + let mut bytes = data.into_vec(); + bytes.reverse(); + Ok(bytes.into()) + } + + fn start(&self, steps: f64) { + for step in 1..=(steps as i32) { + self.events.on_progress(f64::from(step)); + } + self.events.send_event( + "fixtureTick", + Value::Object(vec![("n".into(), Value::Number(1.0))]), + ); + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +#[cfg(test)] +/// The sink is process-wide, so tests that read it take turns. +#[cfg(test)] +static SINK_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +#[cfg(test)] +fn probe() -> std::sync::Arc { + (native_probe_module::().create)(rn_gpui_module::EventEmitter::new(7)) +} + +#[cfg(test)] +fn index(name: &str) -> usize { + native_probe_module::() + .methods + .iter() + .position(|method| method.name == name) + .expect("the spec has this method") +} + +#[cfg(test)] +fn call(name: &str, args: Vec) -> Result { + probe().call_value(index(name), args) +} + +#[test] +fn the_descriptor_describes_the_spec() { + let descriptor = native_probe_module::(); + assert_eq!(descriptor.name, "NativeProbe"); + assert_eq!(descriptor.thread, ModuleThread::Main); + assert_eq!(descriptor.methods.len(), 20); + assert_eq!(descriptor.events.len(), 2); + assert_eq!(descriptor.events[0].name, "onProgress"); + assert!(descriptor.events[0].has_value); + assert!(!descriptor.events[1].has_value); + let lookup = &descriptor.methods[index("lookup")]; + assert_eq!(lookup.callback_args, &[1]); + assert_eq!(lookup.kind, rn_gpui_module::MethodKind::Void); + let done = &descriptor.methods[index("done")]; + assert!(done.returns_void, "Promise resolves with undefined"); + let optional = &descriptor.methods[index("optionalValue")]; + assert_eq!(optional.arg_count, 2); + assert_eq!(optional.required_args, 2); + let nothing = &descriptor.methods[index("nothing")]; + assert_eq!(nothing.arg_count, 0); + assert_eq!(nothing.required_args, 0); +} + +#[test] +fn value_methods_convert_arguments_and_results() { + let value = call( + "scalar", + vec![ + Value::String("text".into()), + Value::Number(2.5), + Value::Number(1.0), + Value::Bool(true), + ], + ) + .expect("scalar works"); + assert!(matches!(value, Value::Bool(true))); + + let value = call( + "optionalValue", + vec![Value::Null, Value::String("here".into())], + ) + .expect("optionalValue works"); + assert!(matches!(value, Value::String(text) if text == "here")); + let value = call( + "optionalValue", + vec![Value::String("a".into()), Value::Null], + ) + .expect("optionalValue works"); + assert!(matches!(value, Value::String(text) if text == "a")); + + let value = call( + "lists", + vec![ + Value::Array(vec![Value::String("ab".into())]), + Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]), + ], + ) + .expect("lists works"); + assert!(matches!(value, Value::Array(items) if items.len() == 1)); + + let options = Value::Object(vec![ + ("type".into(), Value::String("first".into())), + ("label".into(), Value::String("first".into())), + ("count".into(), Value::Number(2.0)), + ("mode".into(), Value::String("right".into())), + ("ratio".into(), Value::Null), + ( + "nested".into(), + Value::Object(vec![ + ("flag".into(), Value::Bool(true)), + ( + "names".into(), + Value::Array(vec![Value::String("n".into())]), + ), + ]), + ), + ]); + let second = Value::Object(vec![ + ("type".into(), Value::String("second".into())), + ("count".into(), Value::Number(3.0)), + ("mode".into(), Value::String("left".into())), + ( + "nested".into(), + Value::Object(vec![ + ("flag".into(), Value::Bool(false)), + ("names".into(), Value::Array(Vec::new())), + ]), + ), + ]); + let value = call("options", vec![options, second]).expect("options works"); + let Value::Object(fields) = &value else { + panic!("options returns an object"); + }; + let field = |name: &str| { + fields + .iter() + .find(|(key, _)| key == name) + .map(|(_, value)| value) + .expect("field") + }; + assert!(matches!(field("label"), Value::String(text) if text == "first")); + assert!(matches!(field("count"), Value::Number(count) if *count == 5.0)); + assert!(matches!(field("mode"), Value::String(mode) if mode == "right")); + + let value = call("level", vec![Value::String("high".into())]).expect("level works"); + assert!(matches!(value, Value::String(mode) if mode == "right")); + + let value = call( + "anything", + vec![Value::Object(vec![("a".into(), Value::Number(1.0))])], + ) + .expect("anything works"); + assert!(matches!(value, Value::Object(fields) if fields.len() == 1)); +} + +#[test] +fn void_and_promise_methods_go_through_their_own_entry_points() { + let _guard = SINK_LOCK.lock().unwrap(); + let _ = rn_gpui_module::sink_calls(); + let module = probe(); + module + .clone() + .prepare_void(index("nothing"), Vec::new()) + .expect("nothing converts")(); + module + .clone() + .prepare_void( + index("inline"), + vec![Value::Object(vec![ + ("x".into(), Value::Number(1.5)), + ("y".into(), Value::String("why".into())), + ])], + ) + .expect("inline converts")(); + + let future = module + .clone() + .call_promise(index("later"), vec![Value::String("gpui".into())]) + .expect("later builds a future"); + let value = futures::executor::block_on(future).expect("later resolves"); + assert!(matches!(value, Value::String(text) if text == "later gpui")); + + let future = module + .clone() + .call_promise(index("done"), Vec::new()) + .expect("done builds a future"); + let value = futures::executor::block_on(future).expect("done resolves"); + assert!(matches!(value, Value::Null), "Promise resolves null"); + let events = rn_gpui_module::sink_calls(); + assert!( + events.iter().any(|call| matches!( + call, + rn_gpui_module::SinkCall::EmitEvent { + event, + value: None, + .. + } if event == "onDone" + )), + "an event without a payload reaches the host" + ); +} + +#[test] +fn callbacks_and_events_reach_the_host() { + let _guard = SINK_LOCK.lock().unwrap(); + let _ = rn_gpui_module::sink_calls(); + let module = probe(); + module + .clone() + .prepare_void( + index("lookup"), + vec![ + Value::String("gpui".into()), + Value::Callback(rn_gpui_module::RawCallback::new(21)), + ], + ) + .expect("lookup converts")(); + module + .clone() + .prepare_void(index("start"), vec![Value::Number(2.0)]) + .expect("start converts")(); + + let calls = rn_gpui_module::sink_calls(); + let invoked = calls.iter().find_map(|call| match call { + rn_gpui_module::SinkCall::InvokeCallback { id, args } => Some((*id, args)), + _ => None, + }); + let (id, args) = invoked.expect("the callback was invoked"); + assert_eq!(id, 21); + assert!(matches!(&args[0], Value::String(text) if text == "value of gpui")); + assert!(matches!(&args[1], Value::Bool(true))); + let progress: Vec = calls + .iter() + .filter_map(|call| match call { + rn_gpui_module::SinkCall::EmitEvent { event, value, .. } if event == "onProgress" => { + match value { + Some(Value::Number(number)) => Some(*number), + _ => None, + } + } + _ => None, + }) + .collect(); + assert_eq!(progress, vec![1.0, 2.0]); + assert!(calls.iter().any(|call| matches!( + call, + rn_gpui_module::SinkCall::SendEvent { name, .. } if name == "fixtureTick" + ))); +} + +#[test] +fn an_array_buffer_crosses_as_bytes() { + let value = call("sum", vec![Value::Buffer(vec![1, 2, 3, 250])]).expect("sum reads the bytes"); + assert!(matches!(value, Value::Number(total) if total == 256.0)); + + let value = + call("reversed", vec![Value::Buffer(vec![1, 2, 3])]).expect("reversed builds a buffer"); + assert!(matches!(value, Value::Buffer(bytes) if bytes == vec![3, 2, 1])); + + let error = + call("sum", vec![Value::Array(Vec::new())]).expect_err("an array is not an ArrayBuffer"); + assert_eq!( + error.message, + "NativeProbe.sum: argument 0 (data): expected ArrayBuffer, got array" + ); +} + +#[test] +fn a_callback_on_a_promise_method_is_invoked_while_it_runs() { + let _guard = SINK_LOCK.lock().unwrap(); + let _ = rn_gpui_module::sink_calls(); + let module = probe(); + + let future = module + .clone() + .call_promise( + index("lookupLater"), + vec![ + Value::String("gpui".into()), + Value::Callback(rn_gpui_module::RawCallback::new(31)), + ], + ) + .expect("lookupLater builds a future"); + let value = futures::executor::block_on(future).expect("lookupLater resolves"); + assert!(matches!(value, Value::String(text) if text == "called")); + + // A callback the method does not call is released without being invoked. + let future = module + .clone() + .call_promise( + index("lookupLater"), + vec![ + Value::String("missing".into()), + Value::Callback(rn_gpui_module::RawCallback::new(32)), + ], + ) + .expect("lookupLater builds a future"); + let value = futures::executor::block_on(future).expect("lookupLater resolves"); + assert!(matches!(value, Value::String(text) if text == "dropped")); + + let calls = rn_gpui_module::sink_calls(); + let invoked: Vec = calls + .iter() + .filter_map(|call| match call { + rn_gpui_module::SinkCall::InvokeCallback { id, args } + if matches!(&args[0], Value::String(text) if text == "later value of gpui") => + { + Some(*id) + } + _ => None, + }) + .collect(); + assert_eq!(invoked, vec![31], "only the found key invokes its callback"); + let dropped: Vec = calls + .iter() + .filter_map(|call| match call { + rn_gpui_module::SinkCall::DropCallback { id } => Some(*id), + _ => None, + }) + .collect(); + assert_eq!(dropped, vec![32], "the other callback is dropped"); +} + +#[test] +fn a_wrong_argument_reports_the_module_method_and_field() { + let error = call("options", vec![Value::Number(1.0), Value::Number(1.0)]) + .expect_err("a wrong argument type is an error"); + assert_eq!( + error.message, + "NativeProbe.options: argument 0 (value): expected object, got number" + ); + + let error = call( + "options", + vec![ + Value::Object(vec![ + ("type".into(), Value::String("first".into())), + ("count".into(), Value::String("nope".into())), + ("mode".into(), Value::String("left".into())), + ( + "nested".into(), + Value::Object(vec![ + ("flag".into(), Value::Bool(true)), + ("names".into(), Value::Array(Vec::new())), + ]), + ), + ]), + Value::Object(vec![ + ("type".into(), Value::String("second".into())), + ("count".into(), Value::Number(1.0)), + ("mode".into(), Value::String("left".into())), + ( + "nested".into(), + Value::Object(vec![ + ("flag".into(), Value::Bool(true)), + ("names".into(), Value::Array(Vec::new())), + ]), + ), + ]), + ], + ) + .expect_err("a wrong field type is an error"); + assert_eq!( + error.message, + "NativeProbe.options: argument 0 (value).count: expected Int32, got string" + ); + + let error = call("level", vec![Value::String("sideways".into())]).expect_err("bad enum"); + assert_eq!( + error.message, + "NativeProbe.level: argument 0 (level): expected one of \"low\", \"high\", got \"sideways\"" + ); + + let error = call("scalar", Vec::new()).expect_err("missing argument"); + assert_eq!( + error.message, + "NativeProbe.scalar: argument 0 (text) is missing" + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/module.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/module.rs new file mode 100644 index 0000000000..cc9614bcae --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-module/tests/module.rs @@ -0,0 +1,419 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use rn_gpui_module::{ + descriptor, register, sink_calls, spawn_main, EventEmitter, EventInfo, FromValue, IntoValue, + Job, MethodInfo, MethodKind, ModuleDescriptor, ModuleError, ModuleInstance, ModuleQueue, + ModuleThread, Path, RawCallback, SinkCall, Value, +}; + +/// The sink is process-wide, so tests that read it take turns. +static SINK_LOCK: Mutex<()> = Mutex::new(()); + +fn read(value: Value, module: &'static str, method: &'static str) -> T { + let mut path = Path::argument(module, method, 0, "value"); + T::from_value(value, &mut path).expect("value converts") +} + +fn read_err(value: Value) -> ModuleError { + let mut path = Path::argument("Fixture", "summarize", 1, "options"); + match T::from_value(value, &mut path) { + Ok(_) => panic!("expected a conversion error"), + Err(error) => error, + } +} + +#[test] +fn values_round_trip_through_every_type() { + assert_eq!(read::(Value::String("hi".into()), "M", "m"), "hi"); + assert_eq!(read::(Value::Number(1.5), "M", "m"), 1.5); + assert_eq!(read::(Value::Number(7.0), "M", "m"), 7); + assert!(read::(Value::Bool(true), "M", "m")); + assert_eq!(read::<()>(Value::Null, "M", "m"), ()); + assert_eq!(read::>(Value::Null, "M", "m"), None); + assert_eq!( + read::>(Value::String("x".into()), "M", "m"), + Some("x".to_string()) + ); + assert_eq!( + read::>( + Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]), + "M", + "m" + ), + vec![1, 2] + ); + let nested = Value::Object(vec![( + "tags".to_string(), + Value::Array(vec![Value::String("a".into())]), + )]); + let Value::Object(fields) = nested else { + unreachable!() + }; + assert_eq!(fields.len(), 1); + + assert_eq!(String::from("s").into_value().type_name(), "string"); + assert_eq!(1.5f64.into_value().type_name(), "number"); + assert_eq!(3i32.into_value().type_name(), "number"); + assert_eq!(true.into_value().type_name(), "boolean"); + assert_eq!(().into_value().type_name(), "null"); + assert_eq!(None::.into_value().type_name(), "null"); + assert_eq!( + vec![1i32, 2].into_value().type_name(), + "array", + "Vec converts to an array" + ); + assert_eq!(Value::Null.into_value().type_name(), "null"); +} + +#[test] +fn conversion_errors_name_the_argument_and_field() { + let error = read_err::(Value::Number(1.0)); + assert_eq!( + error.message, + "Fixture.summarize: argument 1 (options): expected string, got number" + ); + + let mut path = Path::argument("Fixture", "summarize", 1, "options"); + let error = rn_gpui_module::__private::field::(&mut path, "label", Value::Number(1.0)) + .expect_err("expected a conversion error"); + assert_eq!( + error.message, + "Fixture.summarize: argument 1 (options).label: expected string, got number" + ); + + let error = read_err::>(Value::Array(vec![Value::Number(2.0)])); + assert_eq!( + error.message, + "Fixture.summarize: argument 1 (options).0: expected string, got number" + ); + + let error = read_err::>(Value::Null); + assert_eq!( + error.message, + "Fixture.summarize: argument 1 (options): expected array, got null" + ); + + let error = read_err::(Value::Number(1.5)); + assert_eq!( + error.message, + "Fixture.summarize: argument 1 (options): expected Int32, got 1.5" + ); + + let error = ModuleError::with_code("E_X", "bad"); + assert_eq!(error.code.as_deref(), Some("E_X")); + assert_eq!(error.to_string(), "bad"); + assert_eq!(ModuleError::from("plain").message, "plain"); + assert_eq!(ModuleError::from(String::from("owned")).message, "owned"); +} + +#[test] +fn callbacks_and_events_reach_the_sink() { + let _guard = SINK_LOCK.lock().unwrap(); + let _ = sink_calls(); + let callback = RawCallback::new(11); + assert_eq!(callback.id(), 11); + callback.invoke(vec![Value::String("value".into()), Value::Bool(true)]); + let dropped = RawCallback::new(12); + drop(dropped); + let events = EventEmitter::new(3); + events.emit("onProgress", Some(Value::Number(1.0))); + events.emit("onDone", None); + events.send_event( + "fixtureTick", + Value::Object(vec![("n".into(), Value::Number(1.0))]), + ); + + let calls = sink_calls(); + assert_eq!(calls.len(), 5, "unexpected calls: {calls:?}"); + match &calls[0] { + SinkCall::InvokeCallback { id, args } => { + assert_eq!(*id, 11); + assert_eq!(args.len(), 2); + assert!(matches!(&args[0], Value::String(text) if text == "value")); + assert!(matches!(&args[1], Value::Bool(true))); + } + other => panic!("expected an invoke, got {other:?}"), + } + assert!(matches!(&calls[1], SinkCall::DropCallback { id } if *id == 12)); + assert!( + matches!(&calls[2], SinkCall::EmitEvent { module, event, value } if *module == 3 && event == "onProgress" && matches!(value, Some(Value::Number(_)))) + ); + assert!( + matches!(&calls[3], SinkCall::EmitEvent { event, value, .. } if event == "onDone" && value.is_none()) + ); + assert!(matches!(&calls[4], SinkCall::SendEvent { name, .. } if name == "fixtureTick")); +} + +struct Fixture { + _events: EventEmitter, + calls: Arc, +} + +impl Fixture { + fn greet(&self, name: String) -> Result { + self.calls.fetch_add(1, Ordering::SeqCst); + Ok(format!("hi {name}")) + } + + fn boom(&self) { + panic!("boom"); + } +} + +impl ModuleInstance for Fixture { + fn call_value(&self, method: usize, args: Vec) -> Result { + match method { + 0 => { + let mut args = rn_gpui_module::__private::Args::new("Fixture", "greet", args); + let name: String = args.next("name")?; + let greeting = rn_gpui_module::__private::catch(|| self.greet(name))??; + Ok(greeting.into_value()) + } + _ => Err(ModuleError::new("Fixture: unknown method")), + } + } + + fn prepare_void( + self: Arc, + method: usize, + _args: Vec, + ) -> Result, ModuleError> { + match method { + 1 => Ok(Box::new(move || { + rn_gpui_module::__private::run_void("Fixture.boom", move || self.boom()) + })), + _ => Err(ModuleError::new("Fixture: unknown method")), + } + } + + fn call_promise( + self: Arc, + _method: usize, + _args: Vec, + ) -> Result>, ModuleError> { + Err(ModuleError::new("Fixture: no promise method")) + } +} + +static FIXTURE_CALLS: Mutex>> = Mutex::new(None); + +fn create_fixture(events: EventEmitter) -> Arc { + let calls = Arc::new(AtomicUsize::new(0)); + *FIXTURE_CALLS.lock().unwrap() = Some(calls.clone()); + Arc::new(Fixture { + _events: events, + calls, + }) +} + +const METHODS: &[MethodInfo] = &[ + MethodInfo { + name: "greet", + arg_count: 1, + required_args: 1, + kind: MethodKind::Value, + returns_void: false, + callback_args: &[], + }, + MethodInfo { + name: "boom", + arg_count: 0, + required_args: 0, + kind: MethodKind::Void, + returns_void: true, + callback_args: &[], + }, +]; + +const EVENTS: &[EventInfo] = &[EventInfo { + name: "onProgress", + has_value: true, +}]; + +#[test] +fn the_registry_finds_a_module_and_rejects_a_duplicate() { + register(ModuleDescriptor { + name: "Fixture", + thread: ModuleThread::Own, + methods: METHODS, + events: EVENTS, + create: create_fixture, + }); + let found = descriptor("Fixture").expect("registered"); + assert_eq!(found.methods.len(), 2); + assert_eq!(found.methods[0].name, "greet"); + assert_eq!(found.thread, ModuleThread::Own); + assert!(descriptor("Missing").is_none()); + + let duplicate = std::panic::catch_unwind(|| { + register(ModuleDescriptor { + name: "Fixture", + thread: ModuleThread::Own, + methods: METHODS, + events: EVENTS, + create: create_fixture, + }) + }); + assert!(duplicate.is_err(), "a duplicate name must panic"); +} + +#[test] +fn a_module_instance_converts_arguments_and_turns_panics_into_errors() { + register(ModuleDescriptor { + name: "FixtureCalls", + thread: ModuleThread::Main, + methods: METHODS, + events: EVENTS, + create: create_fixture, + }); + let module = (descriptor("FixtureCalls").expect("registered").create)(EventEmitter::new(9)); + let value = module + .call_value(0, vec![Value::String("gpui".into())]) + .expect("greet works"); + assert!(matches!(value, Value::String(text) if text == "hi gpui")); + + let error = module + .call_value(0, Vec::new()) + .expect_err("missing argument"); + let _ = &error; + assert_eq!( + error.message, "Fixture.greet: argument 0 (name) is missing", + "the generated glue names the argument" + ); + + let work = module + .clone() + .prepare_void(1, Vec::new()) + .expect("prepared"); + work(); + let calls = sink_calls(); + assert!( + calls.iter().any(|call| matches!( + call, + SinkCall::Log { message } if message == "Fixture.boom: the module panicked: boom" + )), + "unexpected calls: {calls:?}" + ); +} + +#[test] +fn a_panic_becomes_an_error() { + let error = rn_gpui_module::__private::catch(|| panic!("boom")).expect_err("caught"); + assert_eq!(error.message, "the module panicked: boom"); + let ok = rn_gpui_module::__private::catch(|| 7).expect("no panic"); + assert_eq!(ok, 7); + + let error = futures::executor::block_on(async { + rn_gpui_module::__private::catch_async(async { panic!("async boom") }) + .await + .expect_err("caught") + }); + assert_eq!(error.message, "the module panicked: async boom"); +} + +#[test] +fn the_queue_runs_jobs_in_order_on_one_other_thread() { + let queue = ModuleQueue::new("FixtureQueue"); + let caller = std::thread::current().id(); + let threads = Arc::new(Mutex::new(Vec::new())); + let order = Arc::new(Mutex::new(Vec::new())); + let done = Arc::new(AtomicUsize::new(0)); + for index in 0..8 { + let threads = threads.clone(); + let order = order.clone(); + let done = done.clone(); + queue.post(Box::new(move || { + threads.lock().unwrap().push(std::thread::current().id()); + order.lock().unwrap().push(index); + done.fetch_add(1, Ordering::SeqCst); + Box::pin(async {}) + })); + } + let awaited = Arc::new(AtomicUsize::new(0)); + let awaited_job = awaited.clone(); + queue.post(Box::new(move || { + Box::pin(async move { + futures::future::ready(()).await; + std::thread::sleep(Duration::from_millis(20)); + awaited_job.fetch_add(1, Ordering::SeqCst); + }) + })); + let after = awaited.clone(); + queue.post(Box::new(move || { + Box::pin(async move { + assert_eq!( + after.load(Ordering::SeqCst), + 1, + "the awaiting job ran first" + ); + }) + })); + for _ in 0..500 { + if done.load(Ordering::SeqCst) == 8 && awaited.load(Ordering::SeqCst) == 1 { + break; + } + std::thread::sleep(Duration::from_millis(10)); + } + assert_eq!(done.load(Ordering::SeqCst), 8); + assert_eq!(awaited.load(Ordering::SeqCst), 1); + assert_eq!(*order.lock().unwrap(), (0..8).collect::>()); + assert!( + threads.lock().unwrap().iter().all(|id| *id != caller), + "jobs must not run on the caller's thread" + ); + assert_eq!(threads.lock().unwrap().len(), 8); + assert!( + threads + .lock() + .unwrap() + .windows(2) + .all(|pair| pair[0] == pair[1]), + "every job must run on the same thread" + ); +} + +#[test] +fn dropping_the_queue_with_pending_jobs_does_not_hang() { + let queue = ModuleQueue::new("FixtureDrop"); + let started = Arc::new(AtomicUsize::new(0)); + for _ in 0..4 { + let started = started.clone(); + queue.post(Box::new(move || { + Box::pin(async move { + started.fetch_add(1, Ordering::SeqCst); + std::thread::sleep(Duration::from_millis(10)); + }) + })); + } + queue.post(Box::new(|| { + Box::pin(async { + futures::future::pending::<()>().await; + }) + })); + drop(queue); + assert!(started.load(Ordering::SeqCst) <= 4); +} + +#[test] +fn spawn_main_without_a_spawner_drops_the_job() { + let ran = Arc::new(AtomicUsize::new(0)); + let job_ran = ran.clone(); + let job: Job = Box::new(move || { + Box::pin(async move { + job_ran.fetch_add(1, Ordering::SeqCst); + }) + }); + spawn_main(job); + assert_eq!(ran.load(Ordering::SeqCst), 0); +} + +#[test] +fn a_value_callback_is_debug_only() { + let _guard = SINK_LOCK.lock().unwrap(); + let value = Value::Callback(RawCallback::new(1)); + let text = format!("{value:?}"); + assert!(text.contains("RawCallback"), "{text}"); + drop(value); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/Cargo.toml b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/Cargo.toml new file mode 100644 index 0000000000..54daf29498 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rn-gpui-sys" +version = "0.1.0" +edition = "2021" +links = "rn_gpui_core" + +[dependencies] +cxx = { workspace = true } +log = { workspace = true } +rn-gpui-module = { path = "../rn-gpui-module" } + +[build-dependencies] +cxx-build = { workspace = true } +cmake = { workspace = true } +serde_json = { workspace = true } diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/build.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/build.rs new file mode 100644 index 0000000000..75c5013b64 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/build.rs @@ -0,0 +1,1131 @@ +//! The cxx bridge is generated first: the C++ glue CMake compiles includes +//! its header. + +use std::env; +use std::fs::{self, OpenOptions}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Bumped whenever the shape of `.configure-inputs` changes, so an upgrade +/// forces exactly one reconfigure instead of leaving a stale build. +const STAMP_VERSION: &str = "3"; + +/// Environment variables the `cmake` and `cc` crates read while generating the +/// build system, and which they resolve through a target-prefixed name first. +/// The `cmake` crate (`cmake-0.1.58/src/lib.rs`) resolves these in +/// `getenv_target_os` as `_`, `_`, then `TARGET_`/`HOST_`, then ``; the `cc` crate +/// (`cc-1.5.1/src/lib.rs`, `target_envs`) resolves `CC`/`CXX`/flag variables the +/// same way. `configure_env_names` expands every base listed here to all those +/// forms. +const CONFIGURE_ENV_VAR_BASES: &[&str] = &[ + // Compilers and flags the generated project is handed. + "CC", + "CXX", + "CFLAGS", + "CXXFLAGS", + "CMAKE_PREFIX_PATH", + "CMAKE_GENERATOR", + "CMAKE_TOOLCHAIN_FILE", + // Programs the `cmake` crate may invoke. + "CMAKE", + "EMCMAKE", + "EMMAKE", + // Apple SDK selection (read by `cc`, then handed to CMake). + "MACOSX_DEPLOYMENT_TARGET", + "SDKROOT", +]; + +/// Cargo-provided variables the `cmake` crate reads directly at configure time +/// (`cmake-0.1.58/src/lib.rs` lines 134-170, 442-460). These have no useful +/// target-prefixed form, so only the plain name goes into the stamp. +const CONFIGURE_CARGO_VARS: &[&str] = &[ + "OPT_LEVEL", + "DEBUG", + "PROFILE", + "HOST", + "CARGO_CFG_TARGET_OS", + "CARGO_CFG_TARGET_ARCH", + "CARGO_CFG_TARGET_ENV", +]; + +fn main() { + println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=build.rs"); + for var in [ + "RN_GPUI_PREBUILT_DIR", + "RN_GPUI_BUILD_FROM_SOURCE", + "RN_GPUI_PREBUILT_CACHE", + "RN_GPUI_PREBUILT_OFFLINE", + "CARGO_NET_OFFLINE", + "GITHUB_TOKEN", + "GH_TOKEN", + ] { + println!("cargo:rerun-if-env-changed={var}"); + } + watch_configure_envs(); + + let manifest_dir = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + watch_cpp_tree(&manifest_dir.join("cpp")); + let react_gpui_dir = manifest_dir.parent().expect("ReactGpui dir").to_path_buf(); + let package_dir = react_gpui_dir + .parent() + .expect("the platform package dir") + .to_path_buf(); + + // Counting parent directories can only be right in one of the two layouts + // this crate builds in: a repository checkout, or `node_modules` in an app. + // Look the dependency up instead. + let react_native_dir = find_node_module(&package_dir, "react-native").unwrap_or_else(|| { + panic!( + "react-native was not found in any node_modules at or above {}", + package_dir.display() + ) + }); + let react_common_dir = react_native_dir.join("ReactCommon"); + let react_cxx_platform_dir = package_dir.join("ReactCxxPlatform"); + // The app root: the directory that holds `node_modules`. Found from where + // react-native was installed, which is the only anchor that holds in a + // repository checkout and in an app alike. + let node_modules_dir = react_native_dir + .parent() + .expect("react-native lives in a node_modules directory") + .to_path_buf(); + let app_root = node_modules_dir + .parent() + .expect("node_modules has a parent directory") + .to_path_buf(); + for changed_platform_source in [ + "react/io/ImageLoaderModule.cpp", + "react/io/ImageLoaderModule.h", + "react/runtime/ReactCxxTurboModuleProvider.cpp", + "react/runtime/ReactCxxTurboModuleProvider.h", + "react/runtime/ReactHost.cpp", + "react/runtime/PlatformTimerRegistryImpl.cpp", + ] { + println!( + "cargo:rerun-if-changed={}", + react_cxx_platform_dir + .join(changed_platform_source) + .display() + ); + } + let overrides_dir = package_dir.join("ReactCommonOverrides"); + println!("cargo:rerun-if-changed={}", overrides_dir.display()); + + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set")); + let target_triple = env::var("TARGET").expect("TARGET is set"); + let cargo_target_dir = cargo_target_dir(&out_dir, &target_triple); + let cmake_build_type = cmake_build_type(); + let cmake_out_dir = cargo_target_dir + .join("rn-gpui-cmake") + .join(&target_triple) + .join(cmake_build_type); + + let autolink_manifest = env::var_os("RN_GPUI_AUTOLINK_MANIFEST") + .map(PathBuf::from) + .unwrap_or_else(|| app_root.join("desktop/autolink/autolink.json")); + // Generated sources live beside the CMake build tree instead of Cargo's + // OUT_DIR. The OUT_DIR hash changes with the workspace and feature set, and + // a changed include path would make CMake rebuild every C++ file. + let prebuilt_dir = select_prebuilt_dir( + &package_dir, + &target_triple, + &prebuilt_mode(&autolink_manifest), + ); + // A prebuilt build keeps its own CMake directory. Sharing one with source + // builds leaves their librn_gpui_core.a on the linker search path, where it + // shadows the prebuilt archive and mixes two React Native ABIs. + let cmake_out_dir = if prebuilt_dir.is_some() { + cmake_out_dir.with_file_name(format!("{cmake_build_type}-prebuilt")) + } else { + cmake_out_dir + }; + let generated_dir = cmake_out_dir.join("generated"); + let generated_rncore = generated_dir.join("fbreactnativespec"); + let generated_rncore_scratch = generated_dir.join("fbreactnativespec.tmp"); + let cxxbridge_include = generated_dir.join("cxxbridge-include"); + + fs::create_dir_all(&cmake_out_dir).expect("create shared CMake output directory"); + // Two cargo invocations in one tree write the same generated directories + // now, so the lock covers codegen as well as the CMake build. + let cmake_lock = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(cmake_out_dir.join(".configure-build.lock")) + .expect("open shared CMake build lock"); + cmake_lock + .lock() + .expect("lock shared CMake build directory"); + + let fb_spec_codegen = manifest_dir.join("scripts/generate-fbreactnativespec.cjs"); + println!("cargo:rerun-if-changed={}", fb_spec_codegen.display()); + println!( + "cargo:rerun-if-changed={}", + package_dir.join("package.json").display() + ); + println!( + "cargo:rerun-if-changed={}", + react_native_dir.join("package.json").display() + ); + watch_tree(&react_native_dir.join("src")); + watch_tree(&react_native_dir.join("scripts/codegen")); + // Usually a sibling of react-native; a hoisting package manager can put it + // higher, which the lookup covers. + let codegen_package = find_node_module(&package_dir, "@react-native/codegen") + .unwrap_or_else(|| node_modules_dir.join("@react-native/codegen")); + println!( + "cargo:rerun-if-changed={}", + codegen_package.join("package.json").display() + ); + watch_tree(&codegen_package.join("lib")); + // Codegen recreates its output directory, so generate into a scratch dir + // and sync. Rewriting unchanged headers would recompile most of RN. + let status = Command::new("node") + .arg(&fb_spec_codegen) + .arg(&react_native_dir) + .arg(&generated_rncore_scratch) + .status() + .expect("run React Native core Codegen; Node.js must be installed"); + assert!( + status.success(), + "React Native FBReactNativeSpec Codegen failed" + ); + sync_tree(&generated_rncore_scratch, &generated_rncore); + + // The app root: the directory that holds `node_modules`. When autolink ran + // it named the manifest, whose location is the app's `desktop/autolink/`; + // otherwise the `node_modules` that holds react-native is the anchor. + let autolink_cmake = env::var_os("RN_GPUI_AUTOLINK_CMAKE") + .map(PathBuf::from) + .unwrap_or_else(|| app_root.join("desktop/autolink/autolink.cmake")); + let project_root = autolink_cmake + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .unwrap_or(&app_root) + .to_path_buf(); + println!("cargo:rerun-if-env-changed=RN_GPUI_AUTOLINK_MANIFEST"); + println!("cargo:rerun-if-env-changed=RN_GPUI_AUTOLINK_CMAKE"); + println!("cargo:rerun-if-changed={}", autolink_cmake.display()); + // Autolinked CMake projects live outside this crate, so Cargo does not see + // edits to them unless each directory is named here. + for line in fs::read_to_string(&autolink_cmake) + .unwrap_or_default() + .lines() + { + if let Some(directory) = line + .strip_prefix("add_subdirectory(\"") + .and_then(|rest| rest.split('"').next()) + { + println!("cargo:rerun-if-changed={directory}"); + } + } + let cpp_provider = autolink_cmake + .parent() + .unwrap_or(&autolink_cmake) + .join("CppTurboModules.generated.cpp"); + println!("cargo:rerun-if-changed={}", cpp_provider.display()); + println!("cargo:rerun-if-changed={}", autolink_manifest.display()); + if let Ok(manifest) = fs::read_to_string(&autolink_manifest) { + if let Ok(manifest) = serde_json::from_str::(&manifest) { + if let Some(modules) = manifest + .get("cppTurboModules") + .and_then(|value| value.as_array()) + { + // The C++ sources, the specs and the headers generated from + // them: a spec edit only reaches the build through its header. + for key in ["sources", "specs", "headers"] { + for source in modules + .iter() + .filter_map(|module| module.get(key)) + .filter_map(serde_json::Value::as_array) + .flatten() + .filter_map(serde_json::Value::as_str) + { + println!("cargo:rerun-if-changed={source}"); + } + } + } + if let Some(projects) = manifest + .get("cmakeProjects") + .and_then(|value| value.as_array()) + { + for project in projects.iter().filter_map(serde_json::Value::as_str) { + println!( + "cargo:rerun-if-changed={}", + PathBuf::from(project).join("CMakeLists.txt").display() + ); + } + } + // The active engine's Node-API export list, if it ships one. On + // Windows the app's executable has to re-export every Node-API + // function, forwarded to the engine's DLL, because an addon's import + // names the file it comes from and there is no global lookup. Only + // the path and the library name travel here; generating the module + // definition is the app build script's job, because linker arguments + // for an executable only take effect from the crate that builds it. + emit_node_api_exports(&manifest); + } + } + cxx_build::bridge("src/lib.rs") + .std("c++20") + .include("cpp") + .compile("rn_gpui_cxx_bridge"); + + let cxxbridge_generated = out_dir.join("cxxbridge").join("include"); + assert!( + cxxbridge_generated.join("rust").join("cxx.h").is_file(), + "cxx_build did not produce {}", + cxxbridge_generated.join("rust/cxx.h").display() + ); + // cxx_build rewrites its headers every run (copies on Windows), so CMake + // reads a mirror that only changes when the header contents do. + sync_tree(&cxxbridge_generated, &cxxbridge_include); + + let mut defines: Vec<(&str, String)> = vec![ + ("REACT_COMMON_DIR", cmake_path(&react_common_dir)), + ( + "REACT_CXX_PLATFORM_DIR", + cmake_path(&react_cxx_platform_dir), + ), + ("RN_GPUI_OVERRIDES_DIR", cmake_path(&overrides_dir)), + ( + "RN_GPUI_CXXBRIDGE_INCLUDE_DIR", + cmake_path(&cxxbridge_include), + ), + ( + "RN_GPUI_GENERATED_RNCORE_DIR", + cmake_path(&generated_rncore), + ), + ("RN_GPUI_PROJECT_ROOT", cmake_path(&project_root)), + ("RN_GPUI_AUTOLINK_CMAKE", cmake_path(autolink_cmake)), + ]; + // CMake deletes its cache when the compiler path changes, and the configure + // it then re-runs has lost every -D argument and fails. Shells disagree on + // the path (the /usr/bin shim or the Xcode toolchain binary behind it), so + // name the shim unless the user chose a compiler. + if cfg!(target_os = "macos") && target_triple.contains("apple") { + for (variable, tool, env_name) in [ + ("CMAKE_C_COMPILER", "/usr/bin/cc", "CC"), + ("CMAKE_CXX_COMPILER", "/usr/bin/c++", "CXX"), + ] { + println!("cargo:rerun-if-env-changed={env_name}"); + if env::var_os(env_name).is_none() { + defines.push((variable, tool.to_string())); + } + } + } + // ccache for the C++ this build compiles. React Native's own + // `ReactNative-application.cmake` sets the launcher for the app build, but + // this crate configures CMake itself and never includes that file, so + // without this the object cache is unused — CI measured 0 cacheable calls. + if env::var_os("RN_GPUI_DISABLE_CCACHE").is_none() && ccache_available() { + for variable in ["CMAKE_C_COMPILER_LAUNCHER", "CMAKE_CXX_COMPILER_LAUNCHER"] { + defines.push((variable, "ccache".to_string())); + } + } + if msvc_target() { + // Rust always links the release CRT (msvcrt). CMake's Debug config + // would compile with /MDd and the final link then fails on + // _CrtDbgReport / _calloc_dbg, so pin every target, including + // subprojects that declare an older cmake_minimum_required, to /MD. + defines.push(("CMAKE_POLICY_DEFAULT_CMP0091", "NEW".to_string())); + defines.push(("CMAKE_MSVC_RUNTIME_LIBRARY", "MultiThreadedDLL".to_string())); + } + if let Some(dir) = &prebuilt_dir { + defines.push(("RN_GPUI_PREBUILT_DIR", cmake_path(dir))); + defines.push(("RN_GPUI_OVERRIDES_SHA256", overrides_sha256(&package_dir))); + } + + // A configure rewrites flags.make, and every object depends on it, so + // configuring on each build-script run would rebuild the whole C++ tree. + // Configure only when the inputs changed; otherwise run the build, whose + // own check still re-runs CMake when a CMakeLists or the cache changed. + // `RN_GPUI_FORCE_CMAKE_CONFIGURE=1` is the escape hatch when the stamp + // misses an input a future toolchain adds. + let build_dir = cmake_out_dir.join("build"); + let stamp = configure_stamp(&defines, cmake_build_type, &target_triple); + let stamp_path = cmake_out_dir.join(".configure-inputs"); + let generator_output_exists = + build_dir.join("Makefile").is_file() || build_dir.join("build.ninja").is_file(); + let configure = env_flag("RN_GPUI_FORCE_CMAKE_CONFIGURE") + || !fs::read_to_string(&stamp_path).is_ok_and(|previous| previous == stamp) + || !build_dir.join("CMakeCache.txt").is_file() + || !generator_output_exists; + + let dst = if configure { + let mut cmake_config = cmake::Config::new("cpp"); + cmake_config + .out_dir(&cmake_out_dir) + .profile(cmake_build_type); + for (name, value) in &defines { + cmake_config.define(name, value); + } + // rn_gpui_modules links PUBLIC to rn_gpui_core, so this builds core too. + let dst = cmake_config.build_target("rn_gpui_modules").build(); + fs::write(&stamp_path, &stamp).expect("write configure inputs stamp"); + dst + } else { + run_cmake_build(&build_dir, cmake_build_type); + cmake_out_dir + }; + drop(cmake_lock); + + let cmake_build_dir = dst.join("build"); + println!( + "cargo:rustc-link-search=native={}", + cmake_build_dir.display() + ); + println!("cargo:rustc-link-lib=static:-bundle=rn_gpui_core"); + + emit_link_libs( + &cmake_manifest(&cmake_build_dir, "link_libs", cmake_build_type), + &out_dir, + ); + + // Libraries with an `@rpath/...` install name cannot be found from + // `rustc-link-search` alone, and `cargo:rustc-link-arg` from this package + // would not reach the dependent binary. The directories are therefore + // exported as `links` metadata (DEP_RN_GPUI_CORE_RPATHS) for the rn-gpui + // package's build script, which turns them into linker rpath flags. + emit_rpaths(&cmake_manifest( + &cmake_build_dir, + "rpath_dirs", + cmake_build_type, + )); + + match ( + env::var("CARGO_CFG_TARGET_OS").as_deref(), + env::var("CARGO_CFG_TARGET_ENV").as_deref(), + ) { + (Ok("macos"), _) => println!("cargo:rustc-link-lib=c++"), + (Ok("linux"), _) | (Ok("windows"), Ok("gnu")) => { + println!("cargo:rustc-link-lib=stdc++") + } + _ => {} + } + // The static OpenSSL and curl archives call into the Windows cryptography + // and event-log APIs without naming the import libraries (their `.drectve` + // sections are written for a build that links through `nmake`, not through + // CMake's imported targets). An *application* links these anyway because + // its Rust dependency tree brings them in (`ring`, `reqwest`), so the gap + // only shows up when a test binary links this crate on its own — + // `LNK2019: unresolved external symbol __imp_CryptGenRandom`, and so on. + // The CMake targets declare them (OpenSSL::Crypto's interface, `ws2_32` on + // folly), but `emit_link_libs` only understands archives this build + // produced, so the system libraries are named here. + if msvc_target() { + for library in [ + "crypt32", "advapi32", "userenv", "ws2_32", "bcrypt", "dbghelp", + ] { + println!("cargo:rustc-link-lib={library}"); + } + } + if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { + for framework in [ + "Foundation", + "CoreFoundation", + "CoreGraphics", + "AppKit", + "Metal", + "QuartzCore", + // libcurl reads the system proxy settings on macOS through + // `SCDynamicStoreCopyProxies`. Nothing linked this framework until + // the Rust HTTP stack was removed: reqwest had pulled it in + // transitively, and without it the link fails on curl's macos.c. + "SystemConfiguration", + ] { + println!("cargo:rustc-link-lib=framework={framework}"); + } + } +} + +fn sync_tree(src: &Path, dst: &Path) { + fs::create_dir_all(dst).expect("create generated output directory"); + let mut seen = std::collections::HashSet::new(); + for entry in fs::read_dir(src).expect("read generated directory") { + let entry = entry.expect("read generated entry"); + let name = entry.file_name(); + let from = entry.path(); + let to = dst.join(&name); + seen.insert(name); + // metadata() follows cxx_build's symlinks on Unix. + if fs::metadata(&from).expect("stat generated entry").is_dir() { + sync_tree(&from, &to); + continue; + } + let bytes = fs::read(&from).expect("read generated file"); + if to.is_dir() { + fs::remove_dir_all(&to).expect("replace generated directory"); + } + if fs::read(&to).ok().as_deref() != Some(&bytes[..]) { + fs::write(&to, &bytes).expect("write generated file"); + } + } + for entry in fs::read_dir(dst).expect("read generated mirror") { + let entry = entry.expect("read generated mirror entry"); + if !seen.contains(&entry.file_name()) { + let path = entry.path(); + if path.is_dir() { + fs::remove_dir_all(&path).expect("remove stale generated directory"); + } else { + fs::remove_file(&path).expect("remove stale generated file"); + } + } + } +} + +fn watch_tree(root: &Path) { + if !root.is_dir() { + return; + } + println!("cargo:rerun-if-changed={}", root.display()); + for entry in fs::read_dir(root).expect("read codegen input directory") { + let path = entry.expect("read codegen input entry").path(); + if path.is_dir() { + watch_tree(&path); + } else { + println!("cargo:rerun-if-changed={}", path.display()); + } + } +} + +fn watch_cpp_tree(root: &Path) { + if !root.is_dir() { + return; + } + for entry in fs::read_dir(root).expect("read C++ source directory") { + let path = entry.expect("read C++ source entry").path(); + if path.is_dir() { + if matches!( + path.file_name().and_then(|name| name.to_str()), + Some("build" | "target") + ) { + continue; + } + watch_cpp_tree(&path); + continue; + } + + let is_source = matches!( + path.extension().and_then(|ext| ext.to_str()), + Some("c" | "cc" | "cpp" | "cxx" | "h" | "hh" | "hpp" | "hxx" | "cmake") + ) || path + .file_name() + .is_some_and(|name| name == "CMakeLists.txt"); + if is_source { + println!("cargo:rerun-if-changed={}", path.display()); + } + } +} + +/// Everything that changes what CMake configures. When it all matches the +/// previous run, the configure step is skipped: a reconfigure rewrites +/// `flags.make`, and make would then rebuild every object in the tree. +/// +/// This only lists inputs the `cmake` crate reads. `cc` resolves the compiler +/// variables through `target_envs` (see `configure_env_names`), and the values +/// inside `cc`'s own cache depend on `OPT_LEVEL`/`DEBUG`, which Cargo rewrites +/// for the whole build, not just this crate. +fn configure_stamp(defines: &[(&str, String)], profile: &str, target: &str) -> String { + let mut stamp = String::new(); + stamp.push_str("STAMP_VERSION="); + stamp.push_str(STAMP_VERSION); + stamp.push('\n'); + for (name, value) in defines { + stamp.push_str(name); + stamp.push('='); + stamp.push_str(value); + stamp.push('\n'); + } + for name in configure_env_names(target) { + stamp.push_str(&name); + stamp.push('='); + stamp.push_str(&env::var(name).unwrap_or_default()); + stamp.push('\n'); + } + stamp.push_str("profile="); + stamp.push_str(profile); + stamp.push('\n'); + stamp.push_str("target="); + stamp.push_str(target); + stamp.push('\n'); + stamp.push_str("cmake_version="); + stamp.push_str(&cmake_version()); + stamp.push('\n'); + stamp +} + +/// Declares, once, every environment variable that `configure_stamp` reads, so +/// Cargo re-runs the build script when one changes. The target forms are +/// declared for every variable too: `configure_env_names` decides which one +/// actually lands in the stamp, but Cargo only needs to know that the script +/// depends on it. +fn watch_configure_envs() { + println!("cargo:rerun-if-env-changed=RN_GPUI_FORCE_CMAKE_CONFIGURE"); + for name in configure_env_names(&env::var("TARGET").unwrap_or_default()) { + println!("cargo:rerun-if-env-changed={name}"); + } +} + +/// The environment variable names that can change a CMake configure: each base +/// name, plus the target-specific forms the `cmake` and `cc` crates resolve, in +/// the same order they try them (`cmake::Config::getenv_target_os`). Duplicates +/// are removed and the list is sorted so the stamp is stable. +fn configure_env_names(target: &str) -> Vec { + if target.is_empty() { + return Vec::new(); + } + // `cc` replaces both `-` and `.` with `_`. + let target_underscored = target.replace(['-', '.'], "_"); + let kind = if target == env::var("HOST").unwrap_or_default() { + "HOST" + } else { + "TARGET" + }; + let mut names = Vec::new(); + for base in CONFIGURE_ENV_VAR_BASES { + for name in [ + format!("{base}_{target}"), + format!("{base}_{target_underscored}"), + format!("{kind}_{base}"), + (*base).to_string(), + ] { + if !names.contains(&name) { + names.push(name); + } + } + } + for name in CONFIGURE_CARGO_VARS { + let name = (*name).to_string(); + if !names.contains(&name) { + names.push(name); + } + } + names.sort(); + names +} + +/// The first line of `cmake --version`. A different CMake can accept or reject +/// different `-D` arguments, so it belongs in the stamp. Any failure yields an +/// empty string: version parsing never fails the build. +fn cmake_version() -> String { + let executable = env::var("CMAKE").unwrap_or_else(|_| "cmake".to_string()); + Command::new(executable) + .arg("--version") + .output() + .ok() + .filter(|output| output.status.success()) + .and_then(|output| { + String::from_utf8_lossy(&output.stdout) + .lines() + .next() + .map(str::to_string) + }) + .unwrap_or_default() +} + +/// Builds without configuring. CMake's own staleness check re-runs the +/// configure when a CMakeLists or the cache changed, so this is safe as long +/// as the configure inputs match `configure_stamp`. +fn run_cmake_build(build_dir: &Path, profile: &str) { + let mut command = Command::new("cmake"); + command + .arg("--build") + .arg(build_dir) + .arg("--target") + .arg("rn_gpui_modules") + .arg("--config") + .arg(profile); + if let Some(makeflags) = env::var_os("CARGO_MAKEFLAGS") { + command.env("MAKEFLAGS", makeflags); + } else if let Ok(jobs) = env::var("NUM_JOBS") { + command.arg("--parallel").arg(jobs); + } + let status = command + .status() + .expect("run cmake --build; cmake must be on PATH"); + assert!(status.success(), "CMake build of rn_gpui_modules failed"); +} + +/// The app's `project.desktop.prebuilt` option (`auto`, `always` or `never`), which +/// autolink copies into its manifest. A missing manifest or field means `auto`. +fn prebuilt_mode(autolink_manifest: &Path) -> String { + fs::read_to_string(autolink_manifest) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|manifest| manifest.get("prebuilt")?.as_str().map(str::to_owned)) + .unwrap_or_else(|| "auto".to_string()) +} + +/// Order: `RN_GPUI_BUILD_FROM_SOURCE=1` or mode `never` -> source; +/// `RN_GPUI_PREBUILT_DIR` -> use it; otherwise `prebuilt-binaries.mjs ensure` +/// downloads (or reads the cache) the pinned runtime. The pin is used only +/// while the C++ inputs still hash to the pinned input hash, wherever this +/// package is installed. Any failure falls back to source with one warning, +/// and mode `always` turns that failure into a panic. +fn select_prebuilt_dir(package_dir: &Path, target: &str, mode: &str) -> Option { + let dir = select_prebuilt_dir_inner(package_dir, target, mode)?; + match prebuilt_version(&dir) { + Some(version) => { + println!("cargo:warning=rn-gpui-sys: using prebuilt runtime {version} ({target})") + } + None => println!( + "cargo:warning=rn-gpui-sys: using prebuilt runtime {} ({target})", + dir.display() + ), + } + Some(dir) +} +/// The archive's own version, so the build output and the CI report can name +/// the runtime that was linked instead of just its cache directory. +fn prebuilt_version(dir: &Path) -> Option { + let text = fs::read_to_string(dir.join("manifest.json")).ok()?; + let manifest: serde_json::Value = serde_json::from_str(&text).ok()?; + manifest + .get("version") + .and_then(|value| value.as_str()) + .map(str::to_owned) +} + +fn select_prebuilt_dir_inner(package_dir: &Path, target: &str, mode: &str) -> Option { + if env_flag("RN_GPUI_BUILD_FROM_SOURCE") || mode == "never" { + return None; + } + let require = mode == "always"; + if let Some(dir) = env::var_os("RN_GPUI_PREBUILT_DIR") { + let dir = PathBuf::from(dir); + return match check_prebuilt_react_native(&dir, package_dir) { + Ok(()) => Some(dir), + Err(reason) => { + fallback_or_panic(require, &reason); + None + } + }; + } + // Always compare the local C++ inputs with the pin, in a repository checkout + // and under `node_modules` alike. A consumer whose own C++ inputs differ + // must not link an archive those inputs did not build, and the hash is + // computed from paths relative to this package, so it is the same value in + // both layouts. + if let Err(reason) = check_pinned_inputs(package_dir) { + fallback_or_panic(require, &reason); + return None; + } + + let script = package_dir.join("scripts").join("prebuilt-binaries.mjs"); + let output = Command::new("node") + .arg(&script) + .arg("ensure") + .arg("--triple") + .arg(target) + .output(); + match output { + Ok(out) if out.status.success() => { + let dir = String::from_utf8_lossy(&out.stdout).trim().to_string(); + if dir.is_empty() { + fallback_or_panic(require, "prebuilt-binaries.mjs printed no directory"); + return None; + } + let dir = PathBuf::from(dir); + match check_prebuilt_react_native(&dir, package_dir) { + Ok(()) => Some(dir), + Err(reason) => { + fallback_or_panic(require, &reason); + None + } + } + } + Ok(out) => { + let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string(); + fallback_or_panic( + require, + &format!( + "prebuilt-binaries.mjs ensure exited {}: {stderr}", + out.status + ), + ); + None + } + Err(error) => { + fallback_or_panic(require, &format!("could not run node: {error}")); + None + } + } +} + +/// The archive's ReactCommon and react-native headers come from the consumer's +/// own react-native install, so that install must be the version the archive +/// was built against. Compares manifest.json `reactNative` with the installed +/// react-native package.json `version`. Any mismatch (or unreadable input) is an +/// error, which the caller turns into a source fallback. +fn check_prebuilt_react_native(dir: &Path, package_dir: &Path) -> Result<(), String> { + let manifest_path = dir.join("manifest.json"); + let manifest: serde_json::Value = fs::read_to_string(&manifest_path) + .map_err(|error| format!("cannot read {}: {error}", manifest_path.display())) + .and_then(|text| { + serde_json::from_str(&text) + .map_err(|error| format!("invalid {}: {error}", manifest_path.display())) + })?; + let built_for = manifest + .get("reactNative") + .and_then(|value| value.as_str()) + .ok_or_else(|| format!("{} has no reactNative version", manifest_path.display()))?; + + let installed_pkg = find_node_module(package_dir, "react-native") + .map(|dir| dir.join("package.json")) + .filter(|path| path.is_file()) + .ok_or_else(|| { + "no react-native install found at or above the platform package".to_string() + })?; + let installed: serde_json::Value = serde_json::from_str( + &fs::read_to_string(&installed_pkg) + .map_err(|error| format!("cannot read {}: {error}", installed_pkg.display()))?, + ) + .map_err(|error| format!("invalid {}: {error}", installed_pkg.display()))?; + let installed_version = installed + .get("version") + .and_then(|value| value.as_str()) + .unwrap_or(""); + + if installed_version != built_for { + return Err(format!( + "prebuilt runtime was built for react-native {built_for}, installed react-native is {installed_version}" + )); + } + Ok(()) +} + +/// A repository checkout may have C++ edits the pinned archive does not +/// contain, so its inputs must hash to the pin's `inputHash`. +fn check_pinned_inputs(package_dir: &Path) -> Result<(), String> { + let pin_path = package_dir.join("prebuilt-binaries.json"); + println!("cargo:rerun-if-changed={}", pin_path.display()); + let pinned = fs::read_to_string(&pin_path) + .ok() + .and_then(|text| serde_json::from_str::(&text).ok()) + .and_then(|pin| pin.get("inputHash")?.as_str().map(str::to_owned)) + .ok_or_else(|| format!("{} has no inputHash", pin_path.display()))?; + let output = Command::new("node") + .arg(package_dir.join("scripts").join("prebuilt-inputs.mjs")) + .output() + .map_err(|error| format!("could not run node: {error}"))?; + let local = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !output.status.success() || local.is_empty() { + return Err("prebuilt-inputs.mjs failed".to_string()); + } + if local != pinned { + return Err(format!( + "local C++ inputs hash to {local}, the pinned prebuilt was built from {pinned}" + )); + } + Ok(()) +} + +fn fallback_or_panic(require: bool, reason: &str) { + if require { + panic!( + "rn-gpui-sys: project.desktop.prebuilt is 'always' and no prebuilt runtime: {reason}" + ); + } + println!("cargo:warning=rn-gpui-sys: no prebuilt runtime ({reason}); building from source"); +} + +fn env_flag(name: &str) -> bool { + matches!(env::var(name).as_deref(), Ok("1") | Ok("true")) +} + +fn overrides_sha256(package_dir: &Path) -> String { + let output = Command::new("node") + .arg(package_dir.join("scripts").join("prebuilt-inputs.mjs")) + .arg("--overrides-sha256") + .output() + .expect("run node for overridesSha256"); + assert!( + output.status.success(), + "overridesSha256 failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8_lossy(&output.stdout).trim().to_string() +} + +fn cmake_path(path: impl AsRef) -> String { + path.as_ref().to_string_lossy().replace('\\', "/") +} + +/// CMake writes these manifests per configuration because their content names +/// target file directories, and those differ between configurations on +/// multi-config generators (Visual Studio). Fall back to the unsuffixed name so +/// a build directory generated by an older layout still works. +fn cmake_manifest(cmake_build_dir: &Path, stem: &str, build_type: &str) -> PathBuf { + let per_config = cmake_build_dir.join(format!("{stem}-{build_type}.txt")); + if per_config.is_file() { + return per_config; + } + cmake_build_dir.join(format!("{stem}.txt")) +} + +fn emit_rpaths(path: &Path) { + println!("cargo:rerun-if-changed={}", path.display()); + let text = match std::fs::read_to_string(path) { + Ok(text) => text, + Err(error) => { + println!( + "cargo:warning=rn-gpui-sys: could not read {}: {error}", + path.display() + ); + return; + } + }; + + let dirs: Vec<&str> = text + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .collect(); + if dirs.is_empty() { + return; + } + println!("cargo:rpaths={}", dirs.join("|")); +} + +/// The installed `` package, found by walking up from `start` for a +/// `node_modules` directory that holds it. +/// +/// A package's location must never be derived by counting parent directories: +/// this repository keeps its own packages at different depths (`packages/core`, +/// `modules/fonts`), while an app has every one of them in `node_modules`. +/// Walking up is the only rule that holds in both, and it also covers a package +/// hoisted to an ancestor `node_modules` by a workspace. +fn find_node_module(start: &Path, name: &str) -> Option { + for ancestor in start.ancestors() { + let candidate = ancestor.join("node_modules").join(name); + if candidate.join("package.json").is_file() { + return Some(candidate); + } + } + None +} + +/// Exports the active engine's Node-API export list as link metadata. +/// +/// `cargo:node_api_exports` and `cargo:node_api_library` become +/// `DEP_RN_GPUI_CORE_NODE_API_EXPORTS` / `..._NODE_API_LIBRARY` for a dependent +/// `links = "rn_gpui_core"` crate. Nothing is emitted when the engine ships no +/// list (a platform with global symbol lookup, or an engine without Node-API), +/// which is how the app build script tells the two apart. +fn emit_node_api_exports(manifest: &serde_json::Value) { + let Some(engine) = manifest + .get("jsEngines") + .and_then(|value| value.as_array()) + .and_then(|engines| engines.first()) + else { + return; + }; + let Some(exports) = engine.get("nodeApiExports").and_then(|v| v.as_str()) else { + return; + }; + let Some(library) = engine.get("nodeApiLibrary").and_then(|v| v.as_str()) else { + println!( + "cargo:warning=rn-gpui-sys: the JS engine declares nodeApiExports without a nodeApiLibrary; Windows addons will not load" + ); + return; + }; + if !Path::new(exports).is_file() { + println!( + "cargo:warning=rn-gpui-sys: the JS engine's Node-API export list is missing: {exports}" + ); + return; + } + println!("cargo:rerun-if-changed={exports}"); + println!("cargo:node_api_exports={exports}"); + println!("cargo:node_api_library={library}"); +} + +/// Whether a `ccache` binary is on `PATH`. +/// +/// Only used to decide whether to hand CMake the compiler launcher: setting it +/// to a binary that is not there fails the configure, and a machine without +/// ccache has to build without it. +fn ccache_available() -> bool { + let path = match env::var_os("PATH") { + Some(path) => path, + None => return false, + }; + let exe = if cfg!(windows) { + "ccache.exe" + } else { + "ccache" + }; + env::split_paths(&path).any(|dir| dir.join(exe).is_file()) +} + +fn msvc_target() -> bool { + matches!(std::env::var("CARGO_CFG_TARGET_ENV").as_deref(), Ok("msvc")) +} + +/// Translates `link_libs.txt` (written by CMake) into cargo directives. +/// +/// Line forms: +/// * `name:` — a library file name; `-l` takes it stripped of its +/// `lib` prefix and extension, `dylib=` for shared, `static=` for archives. +/// * `search:` — a directory to add to the link search path. +/// +/// Off MSVC, third-party archives are linked through uniquely named symlinks +/// (`librngpui_ssl.a` for `libssl.a`). A crate that finds a system library +/// through pkg-config adds a directory such as `/usr/lib/x86_64-linux-gnu` to +/// the search path ahead of ours, and `-lssl` would then pick up the system's +/// static OpenSSL instead of the one this build's curl was compiled against. +fn emit_link_libs(path: &Path, out_dir: &Path) { + println!("cargo:rerun-if-changed={}", path.display()); + let text = match std::fs::read_to_string(path) { + Ok(text) => text, + Err(error) => { + println!( + "cargo:warning=rn-gpui-sys: could not read {}: {error}", + path.display() + ); + return; + } + }; + + let link_dir = out_dir.join("rn-gpui-link"); + let mut seen = std::collections::HashSet::new(); + let mut last_search: Option<&str> = None; + for line in text.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + if let Some(dir) = line.strip_prefix("search:") { + last_search = Some(dir); + if seen.insert(format!("search:{dir}")) { + println!("cargo:rustc-link-search=native={dir}"); + } + } else if let Some(file) = line.strip_prefix("name:") { + let file = Path::new(file); + let stem = file + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or_default(); + // link.exe only appends `.lib`, so on MSVC the `lib` prefix that + // some libraries really carry (libcurl-d.lib, libssl.lib) must be + // kept; stripping it fails with LNK1181. + let stem = if msvc_target() { + stem + } else { + stem.strip_prefix("lib").unwrap_or(stem) + }; + let is_archive = stem.ends_with(".a") || stem.ends_with(".lib"); + let name = if is_archive { + stem.trim_end_matches(".a").trim_end_matches(".lib") + } else { + stem.split('.').next().unwrap_or(stem) + }; + if name.is_empty() { + continue; + } + // The main archive is emitted before this transitive manifest so + // Cargo does not see conflicting link modifiers for the same lib. + if name == "rn_gpui_core" { + continue; + } + let directive = if is_archive { + let source = last_search.map(|dir| Path::new(dir).join(file)); + match source.and_then(|source| link_unique_archive(&link_dir, name, &source)) { + Some(unique) => { + if seen.insert(format!("search:{}", link_dir.display())) { + println!("cargo:rustc-link-search=native={}", link_dir.display()); + } + format!("static:-bundle={unique}") + } + None => format!("static:-bundle={name}"), + } + } else { + format!("dylib={name}") + }; + if seen.insert(directive.clone()) { + println!("cargo:rustc-link-lib={directive}"); + } + } else { + // A bare name is a static archive. + let directive = format!("static:-bundle={line}"); + if seen.insert(directive.clone()) { + println!("cargo:rustc-link-lib={directive}"); + } + } + } +} + +/// Symlinks `source` into `link_dir` as `librngpui_.a` and returns the +/// `-l` name to use. Returns `None` (link by the original name) on MSVC, for +/// this build's own `rn_gpui_*` archives, or when the archive is not found. +fn link_unique_archive(link_dir: &Path, name: &str, source: &Path) -> Option { + if msvc_target() || name.starts_with("rn_gpui") || !source.is_file() { + return None; + } + #[cfg(unix)] + { + let unique = format!("rngpui_{name}"); + fs::create_dir_all(link_dir).ok()?; + let link = link_dir.join(format!("lib{unique}.a")); + if fs::read_link(&link).ok().as_deref() != Some(source) { + let _ = fs::remove_file(&link); + std::os::unix::fs::symlink(source, &link).ok()?; + } + Some(unique) + } + #[cfg(not(unix))] + { + let _ = link_dir; + None + } +} + +fn cargo_target_dir(out_dir: &Path, target_triple: &str) -> PathBuf { + if let Some(target_dir) = env::var_os("CARGO_TARGET_DIR") { + let target_dir = PathBuf::from(target_dir); + return if target_dir.is_absolute() { + target_dir + } else { + env::current_dir() + .expect("read build-script working directory") + .join(target_dir) + }; + } + + let profile = env::var("PROFILE").expect("PROFILE is set"); + let profile_dir = out_dir + .ancestors() + .find(|path| { + path.file_name() + .is_some_and(|name| name.to_string_lossy() == profile) + }) + .expect("find Cargo profile directory above OUT_DIR"); + let target_or_profile_dir = profile_dir + .parent() + .expect("Cargo profile directory has a parent"); + if target_or_profile_dir + .file_name() + .is_some_and(|name| name == target_triple) + { + target_or_profile_dir + .parent() + .expect("Cargo target triple directory has a parent") + .to_path_buf() + } else { + target_or_profile_dir.to_path_buf() + } +} + +fn cmake_build_type() -> &'static str { + match env::var("OPT_LEVEL").as_deref() { + Ok("0") => "Debug", + Ok("s" | "z") => "MinSizeRel", + _ if env::var("DEBUG").as_deref() == Ok("false") => "Release", + _ => "RelWithDebInfo", + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/CMakeLists.txt new file mode 100644 index 0000000000..7a9aa2c9ca --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/CMakeLists.txt @@ -0,0 +1,1181 @@ +cmake_minimum_required(VERSION 3.18) +project(rn_gpui_core CXX C) +if(APPLE) + enable_language(OBJCXX) +endif() +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +if(MSVC) + # React Native, folly and several bundled third-party projects carry + # clang/gcc assumptions. MSVC needs its own spellings: /utf-8 because fmt and + # ReactCommon contain non-ASCII string literals (fmt asserts with "Unicode + # support requires compiling with /utf-8"), and /EHsc because RN throws and + # catches exceptions while cl.exe leaves exception handling off by default. + add_compile_options( + "$<$:/utf-8>" + "$<$:/EHsc>" + "$<$:/GR>") + # folly and fmt inspect __cplusplus, which MSVC reports as 199711L unless + # /Zc:__cplusplus is given. + add_compile_options("$<$:/Zc:__cplusplus>") + # MSBuild hands each project's sources to a single cl.exe, which compiles + # them one at a time unless /MP is given; project dependencies leave only + # one or two projects building at once. Half the cores per cl.exe keeps the + # machine busy without exhausting memory (each compiler can use ~1 GB). + cmake_host_system_information(RESULT _rn_gpui_cores QUERY NUMBER_OF_LOGICAL_CORES) + math(EXPR _rn_gpui_mp "(${_rn_gpui_cores} + 1) / 2") + add_compile_options("$<$:/MP${_rn_gpui_mp}>") + # ReactCommon headers do `#ifndef RN_EXPORT / #define RN_EXPORT + # __attribute__((visibility("default")))`, which cl.exe cannot parse. The + # guard means defining it empty here is enough. + add_compile_definitions(RN_EXPORT=) + # glog's headers declare every symbol __declspec(dllimport) on Windows unless + # GOOGLE_GLOG_DLL_DECL is already defined, while this build links glog + # statically. Its own `#ifndef` guard lets the static spelling win here. + add_compile_definitions(GOOGLE_GLOG_DLL_DECL=) + # ReactCommon relies on libc++'s transitive includes; force the common + # standard headers into every C++ translation unit on MSVC. + add_compile_options( + "$<$:/FI${CMAKE_CURRENT_SOURCE_DIR}/cmake/msvc-preinclude.h>") + # defines min/max macros that break std::min/std::max in + # ReactCommon and folly, and both expect the Windows 10 API surface. + add_compile_definitions(NOMINMAX) + add_compile_definitions(_WIN32_WINNT=0x0A00) + # Mirrors react-native-windows: __unused is an Apple attribute, JSI_EXPORT + # marks the dllimport storage class in jsi's headers (jsi exports come from + # WINDOWS_EXPORT_ALL_SYMBOLS below), + # _CRT_SECURE_NO_WARNINGS silences CRT deprecation warnings, and + # _DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR avoids the MSVC 14.4x constexpr + # std::mutex problem. BOOST_ALL_NO_LIB stops boost headers from asking the + # linker for boost libraries we do not build. + add_compile_definitions("__unused=[[maybe_unused]]") + add_compile_definitions(JSI_EXPORT=) + add_compile_definitions(_CRT_SECURE_NO_WARNINGS) + add_compile_definitions(_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR) + add_compile_definitions(BOOST_ALL_NO_LIB=1) + # Lets boost compile its error_code/system facilities header-only so that + # nothing asks the linker for boost_system.lib; react-native-windows + # defines the same set. + add_compile_definitions(BOOST_ERROR_CODE_HEADER_ONLY) + add_compile_definitions(BOOST_SYSTEM_SOURCE) + add_compile_definitions(BOOST_NO_RTTI) + add_compile_definitions(BOOST_NO_TYPEID) + # pulls in wingdi.h, whose BLACK and TRANSPARENT macros collide + # with the names bundled libraries use for colour constants (one defines + # `const std::array BLACK` in its interpolator registry), and + # WIN32_LEAN_AND_MEAN keeps the rest of the Win32 surface out of reach. + add_compile_definitions(WIN32_LEAN_AND_MEAN) + add_compile_definitions(NOGDI) +endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + # One force-included header stands in for the libc++ headers Android's build + # gets transitively (see the file). It is a single `-include` because CMake + # de-duplicates repeated options: two `-include
` pairs collapse into + # `-include `, and the second header is then read as a stray + # linker input. + add_compile_options( + "$<$:-include;${CMAKE_CURRENT_LIST_DIR}/rn_gpui_linux_prefix.h>") +endif() +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # ReactCommon writes both `class RN_EXPORT [[deprecated(...)]] X`, which GCC + # before 13 rejects with RN_EXPORT's `__attribute__` spelling, and + # `extern RN_EXPORT T x`, which rejects the `[[gnu::]]` spelling. Nothing here + # builds with hidden visibility and ReactCommon links statically, so the + # attribute is a no-op; define it empty as on Windows. + add_compile_definitions(RN_EXPORT=) + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 14) + # ConcreteShadowNode.h redeclares names it already used ("changes meaning"), + # a hard error before GCC 14 and a warning since; -fpermissive matches 14. + add_compile_options("$<$:-fpermissive>") + endif() +endif() + +# --- Windows unity build ------------------------------------------------- +# +# MSBuild hands a project's sources to one cl.exe, which compiles them one at a +# time unless /MP is given, and ReactCommon splits into ~76 projects of ~4 +# sources each while project dependencies leave only one or two building at +# once (see the /MP comment above). Nearly all of that time goes into parsing +# the shared ReactCommon, folly and boost headers, once per source file. A unity +# build merges a target's sources into one translation unit, so those headers +# are parsed once per target instead of once per source. +# +# This is Windows-only and opt-out. MSVC is the only compiler here that is +# driven by MSBuild at project granularity; the Linux and macOS builds already +# compile at target granularity in parallel, where a unity build would only take +# parallelism away and change nothing else. +# +# To turn it off, pass the define through to CMake — `build.rs` does not forward +# arbitrary `RN_GPUI_*` environment variables, so a bare +# `RN_GPUI_UNITY_BUILD=OFF` never reaches this file: +# +# CMAKE_ARGS=-DRN_GPUI_UNITY_BUILD=OFF cargo build (with a reconfigure) +# +# or configure `cpp/` by hand with the same -D. +# +# A unity build changes what the compiler sees in one translation unit, so file +# scope names and macros from one source become visible to the next. The +# vendored third-party targets that cannot tolerate that are switched back off +# with `UNITY_BUILD OFF` in the MSVC section at the end of this file, once every +# target exists. +option(RN_GPUI_UNITY_BUILD + "Merge each MSVC target's sources into unity translation units" ON) +if(MSVC AND RN_GPUI_UNITY_BUILD) + set(CMAKE_UNITY_BUILD ON) + # Merge a target's sources in batches of this size. Most ReactCommon targets + # have fewer sources than this, so they collapse into a single compilation; + # /MP still spreads the batches of the larger targets across cores. + set(CMAKE_UNITY_BUILD_BATCH_SIZE 8) + # A merged translation unit holds the code and debug info of every source in + # its batch, which overflows the COFF section limit on the app's generated + # TurboModule specs ("error C1128: number of sections exceeded object file + # format limit"). /bigobj raises the limit; it is a no-op for units that stay + # under it, so it is set for the whole unity build rather than per target. + add_compile_options("$<$:/bigobj>") +endif() + +# Paths to this package's own trees, published for the autolinked library +# CMakeLists files. They are derived from this file's location, so they stay +# right wherever the package is installed: a library must never count folders +# up to reach core, because the two sit at different depths in an app +# (`node_modules/@react-native-gpui/`) and in this repository. +set(RN_GPUI_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../..") +set(RN_GPUI_SYS_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}") +set(RN_GPUI_REACT_CXX_PLATFORM_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../ReactCxxPlatform") + +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/NodeModules.cmake) + +rn_gpui_find_node_module(react-native _rn_gpui_react_native_dir) +set(REACT_COMMON_DIR "${_rn_gpui_react_native_dir}/ReactCommon") +if(NOT RN_GPUI_GENERATED_RNCORE_DIR) + message(FATAL_ERROR + "RN_GPUI_GENERATED_RNCORE_DIR is required; generate FBReactNativeSpec into the shared CMake output directory") +endif() +set(REACT_CXX_PLATFORM_DIR "${RN_GPUI_REACT_CXX_PLATFORM_DIR}") +if(NOT RN_GPUI_OVERRIDES_DIR) + set(RN_GPUI_OVERRIDES_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/../../../ReactCommonOverrides") +endif() + +# The jsinspector-modern CMakeLists includes SoMerging-utils.cmake from the +# Android tree. That helper defines target_merge_so(), which is a no-op on +# non-Android platforms; the shim contains only that CMake helper, not Android +# runtime code. See android-shim/README.md. +set(REACT_ANDROID_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android-shim") + +# RN Android stores source archives and prepared third-party sources under its +# Gradle build directory. Mirror that layout under this target's CMake build +# cache so independent desktop builds do not rely on host package managers. +# Both modes download and prepare the pinned native-dep sources. Only source +# mode builds OpenSSL and curl (cmake/DownloadNativeDeps.cmake guards those). +set(REACT_ANDROID_SOURCE_DIR "${REACT_COMMON_DIR}/../ReactAndroid") +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/DownloadNativeDeps.cmake) + +# React Native's own CMake projects are added from here in both modes: a source +# build adds the ones core needs, and either mode adds the ones a JS engine +# package declares (RN_GPUI_JS_ENGINE_SUBDIRECTORIES). +function(add_react_common_subdir relative_path) + add_subdirectory(${REACT_COMMON_DIR}/${relative_path} + ReactCommon/${relative_path}) +endfunction() + +function(add_react_cxx_platform_subdir relative_path) + add_subdirectory(${REACT_CXX_PLATFORM_DIR}/${relative_path} + ReactCxxPlatform/${relative_path}) +endfunction() + +if(RN_GPUI_PREBUILT_DIR) + # Prebuilt mode: rn_gpui_core, ReactCommon and third-party come from the + # archive. Only the app-side targets below are compiled here. + set(_rn_gpui_prebuilt_manifest "${RN_GPUI_PREBUILT_DIR}/manifest.json") + if(NOT EXISTS "${_rn_gpui_prebuilt_manifest}") + message(FATAL_ERROR "RN_GPUI_PREBUILT_DIR has no manifest.json: ${RN_GPUI_PREBUILT_DIR}") + endif() + file(READ "${_rn_gpui_prebuilt_manifest}" _rn_gpui_prebuilt_json) + string(JSON _rn_gpui_prebuilt_bridge_sha GET "${_rn_gpui_prebuilt_json}" cxxBridgeHeaderSha256) + string(JSON _rn_gpui_prebuilt_triple GET "${_rn_gpui_prebuilt_json}" triple) + if(RN_GPUI_CXXBRIDGE_INCLUDE_DIR AND EXISTS "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}/rn-gpui-sys/src/lib.rs.h") + file(SHA256 "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}/rn-gpui-sys/src/lib.rs.h" _rn_gpui_bridge_sha) + if(NOT _rn_gpui_bridge_sha STREQUAL _rn_gpui_prebuilt_bridge_sha) + message(FATAL_ERROR + "RN_GPUI_PREBUILT_DIR was built for a different cxx bridge (manifest ${_rn_gpui_prebuilt_bridge_sha}, " + "current ${_rn_gpui_bridge_sha}). Rebuild the prebuilt or unset RN_GPUI_PREBUILT_DIR.") + endif() + endif() + # The archive is Release, so its ReactCommon headers select the production + # ABI (Sealable without a debug constructor). Consumers must compile the same + # way or they reference symbols the archive never exports. + string(JSON _rn_gpui_prebuilt_rn_debug ERROR_VARIABLE _rn_gpui_json_err GET "${_rn_gpui_prebuilt_json}" defines REACT_NATIVE_DEBUG) + if(NOT _rn_gpui_json_err AND _rn_gpui_prebuilt_rn_debug STREQUAL "0") + add_compile_definitions(REACT_NATIVE_PRODUCTION) + endif() + # The app's own C++ is compiled by this configure, in whatever Cargo profile + # the caller used, while the archive is fixed Release (/MD, iterator debug + # level 0). A Cargo debug build configures CMake as Debug, which defines + # _DEBUG; MSVC's STL then expands that to _ITERATOR_DEBUG_LEVEL=2 and selects + # the debug CRT, and the link dies on LNK2038 with + # mismatch detected for '_ITERATOR_DEBUG_LEVEL': value '2' doesn't match '0' + # mismatch detected for 'RuntimeLibrary': 'MDd_DynamicDebug' vs 'MD_DynamicRelease' + # So match the runtime library recorded in the manifest whatever the profile, + # the same way REACT_NATIVE_PRODUCTION is taken from the manifest above. + # `build.rs` already pins CMAKE_MSVC_RUNTIME_LIBRARY to MultiThreadedDLL; the + # iterator level and _DEBUG are what is left. + if(MSVC) + string(JSON _rn_gpui_prebuilt_iterator ERROR_VARIABLE _rn_gpui_iterator_err + GET "${_rn_gpui_prebuilt_json}" flags msvcIteratorDebugLevel) + if(_rn_gpui_iterator_err) + set(_rn_gpui_prebuilt_iterator 0) + endif() + # /U_DEBUG: without it the STL header sees _DEBUG and raises the level again + # even when _ITERATOR_DEBUG_LEVEL is defined. /DNDEBUG keeps assert() off, as + # in the archive; the definition is conditional so a Release consumer does + # not warn about redefining it. + add_compile_options( + "/U_DEBUG" + "$<$>:/DNDEBUG>") + add_compile_definitions(_ITERATOR_DEBUG_LEVEL=${_rn_gpui_prebuilt_iterator}) + endif() + # ReactCommonOverrides is compiled into the archive. build.rs computes the + # current hash (CMake strings cannot hold the NUL separator) and passes it in + # as RN_GPUI_OVERRIDES_SHA256; a mismatch means the archive is stale. + string(JSON _rn_gpui_prebuilt_overrides_sha GET "${_rn_gpui_prebuilt_json}" overridesSha256) + if(RN_GPUI_OVERRIDES_SHA256 AND NOT RN_GPUI_OVERRIDES_SHA256 STREQUAL _rn_gpui_prebuilt_overrides_sha) + message(FATAL_ERROR + "RN_GPUI_PREBUILT_DIR was built from different ReactCommonOverrides (manifest " + "${_rn_gpui_prebuilt_overrides_sha}, current ${RN_GPUI_OVERRIDES_SHA256}). Rebuild the " + "prebuilt or unset RN_GPUI_PREBUILT_DIR.") + endif() + # Headers are not in the archive. Point each root the archive names at the + # tree this build already uses (the same variables source mode compiles with). + set(RN_GPUI_PREBUILT_ROOT_react_native_gpui "${CMAKE_CURRENT_SOURCE_DIR}") + set(RN_GPUI_PREBUILT_ROOT_react_common_overrides "${RN_GPUI_OVERRIDES_DIR}") + set(RN_GPUI_PREBUILT_ROOT_react_common "${REACT_COMMON_DIR}") + set(RN_GPUI_PREBUILT_ROOT_react_cxx_platform "${REACT_CXX_PLATFORM_DIR}") + set(RN_GPUI_PREBUILT_ROOT_generated_rncore "${RN_GPUI_GENERATED_RNCORE_DIR}/jni") + set(RN_GPUI_PREBUILT_ROOT_cxxbridge "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}") + set(RN_GPUI_PREBUILT_ROOT_folly "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly") + set(RN_GPUI_PREBUILT_ROOT_glog "${RN_GPUI_NATIVE_DEPS_PREPARED}/glog/exported") + set(RN_GPUI_PREBUILT_ROOT_fmt "${RN_GPUI_NATIVE_DEPS_PREPARED}/fmt/include") + set(RN_GPUI_PREBUILT_ROOT_double_conversion "${RN_GPUI_NATIVE_DEPS_PREPARED}/double-conversion") + set(RN_GPUI_PREBUILT_ROOT_fast_float "${RN_GPUI_NATIVE_DEPS_PREPARED}/fast_float/include") + set(RN_GPUI_PREBUILT_ROOT_nlohmann_json "${RN_GPUI_NATIVE_DEPS_PREPARED}/nlohmann_json/include") + set(RN_GPUI_PREBUILT_ROOT_boost "${RN_GPUI_NATIVE_DEPS_PREPARED}/boost/boost_1_83_0") + set(RN_GPUI_PREBUILT_ROOT_curl "${RN_GPUI_CURL_SOURCE_DIR}/include") + message(STATUS "rn_gpui_core: using prebuilt runtime ${RN_GPUI_PREBUILT_DIR} (${_rn_gpui_prebuilt_triple})") + find_package(react-native-gpui-prebuilt CONFIG REQUIRED + PATHS "${RN_GPUI_PREBUILT_DIR}/cmake" NO_DEFAULT_PATH) +else() + +# GpuiHttpClient uses libcurl. Build curl from its pinned source archive against +# the OpenSSL copy prepared in the same CMake cache above. +set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(CURL_USE_OPENSSL ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE) +set(CURL_USE_LIBPSL OFF CACHE BOOL "" FORCE) +set(CURL_USE_LIBSSH2 OFF CACHE BOOL "" FORCE) +set(CURL_ZLIB OFF CACHE STRING "" FORCE) +set(USE_NGHTTP2 OFF CACHE BOOL "" FORCE) +set(USE_LIBIDN2 OFF CACHE BOOL "" FORCE) +set(CURL_USE_BROTLI OFF CACHE BOOL "" FORCE) +set(CURL_USE_ZSTD OFF CACHE BOOL "" FORCE) +set(CURL_DISABLE_TESTS ON CACHE BOOL "" FORCE) +add_subdirectory("${RN_GPUI_CURL_SOURCE_DIR}" "${RN_GPUI_NATIVE_DEPS_BUILD}/curl") + +include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake) + +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/ThirdParty.cmake) + +# The cxx-generated bridge header carries the flat +# structs and the `rn_gpui_*` entry point declarations. build.rs runs +# `cxx_build::bridge` *before* configuring CMake and passes the directory here. +# When CMake is driven by hand (the 1.3 workflow) the variable is unset, so fall +# back to the include directory of the most recent cargo build. +# +# This has to happen before the ReactCommon subdirectories are added: the +# rn_gpui_core glue (GpuiHost.cpp, MountBatchBuilder.cpp) needs the bridge +# header. The ReactCommon override does not; it uses GpuiTextMeasure.h. +if(NOT RN_GPUI_CXXBRIDGE_INCLUDE_DIR) + file(GLOB RN_GPUI_CXXBRIDGE_CANDIDATES + "${CMAKE_CURRENT_SOURCE_DIR}/../../target/*/build/rn-gpui-sys-*/out/cxxbridge/include/rust/cxx.h") + set(_newest_stamp "") + foreach(_candidate IN LISTS RN_GPUI_CXXBRIDGE_CANDIDATES) + file(TIMESTAMP "${_candidate}" _stamp "%Y%m%d%H%M%S") + if(_stamp STRGREATER _newest_stamp) + set(_newest_stamp "${_stamp}") + get_filename_component(RN_GPUI_CXXBRIDGE_INCLUDE_DIR "${_candidate}" DIRECTORY) + get_filename_component(RN_GPUI_CXXBRIDGE_INCLUDE_DIR "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}" DIRECTORY) + endif() + endforeach() + if(RN_GPUI_CXXBRIDGE_INCLUDE_DIR) + message(STATUS "rn_gpui_core: cxx bridge headers found at ${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}") + endif() +endif() + +# Shared glue headers for every ReactCommon subdirectory: the TextLayoutManager +# override includes GpuiTextMeasure.h from here. This is fixed platform glue, +# not an app or Cargo input, so it stays directory-wide. The cxx bridge header +# is not added directory-wide; only rn_gpui_core consumes it (see below), so +# the ReactCommon targets build without RN_GPUI_CXXBRIDGE_INCLUDE_DIR. +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + +# Header overrides. Must come before the ReactCommon subdirectories +# below: this directory holds a replacement for +# (a +# platform header that RN finds through the platform include directory), and +# directory-level include dirs are emitted ahead of a target's own ones. +include_directories(${RN_GPUI_OVERRIDES_DIR}) + +# Adding a replacement header changes include resolution without changing the +# upstream header already recorded in compiler dependencies. Recompile every +# target when the override inventory changes to keep RN prop classes ABI-safe. +file(GLOB_RECURSE RN_GPUI_OVERRIDE_HEADERS CONFIGURE_DEPENDS + RELATIVE "${RN_GPUI_OVERRIDES_DIR}" "${RN_GPUI_OVERRIDES_DIR}/*.h") +list(SORT RN_GPUI_OVERRIDE_HEADERS) +string(SHA256 RN_GPUI_OVERRIDE_HEADER_SET "${RN_GPUI_OVERRIDE_HEADERS}") +add_compile_definitions("RN_GPUI_HEADER_OVERRIDES_${RN_GPUI_OVERRIDE_HEADER_SET}=1") + +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/jni react_codegen_rncore) + +add_react_common_subdir(yoga) + +# On Android the `yoga` target (JNI bindings over yogacore) is defined +# in ReactAndroid/src/main/jni/first-party/yogajni, and RN targets link +# it by name to get yogacore's public include directories. The gpui +# build has no JNI layer, so provide a bare target that carries +# yogacore's includes (and links it at final link time). +if(NOT TARGET yoga) + add_library(yoga INTERFACE) + target_link_libraries(yoga INTERFACE yogacore) +endif() +add_react_common_subdir(runtimeexecutor) +add_react_common_subdir(reactperflogger) +add_react_common_subdir(logger) +add_react_common_subdir(jsiexecutor) +add_react_common_subdir(jsitooling) +add_react_common_subdir(cxxreact) +add_react_common_subdir(jsi) +add_react_common_subdir(callinvoker) +add_react_common_subdir(oscompat) +add_react_common_subdir(jsinspector-modern) +add_react_common_subdir(jsinspector-modern/cdp) +add_react_common_subdir(jsinspector-modern/network) +add_react_common_subdir(jsinspector-modern/tracing) +add_react_common_subdir(react/cxxstableapi) +add_react_common_subdir(react/renderer/runtimescheduler) +add_react_common_subdir(react/debug) +add_react_common_subdir(react/featureflags) +add_react_common_subdir(react/performance/cdpmetrics) +add_react_common_subdir(react/performance/timeline) +add_react_common_subdir(react/renderer/animated) +add_react_common_subdir(react/renderer/animationbackend) +add_react_common_subdir(react/renderer/animations) +add_react_common_subdir(react/renderer/attributedstring) +add_react_common_subdir(react/renderer/componentregistry) +add_react_common_subdir(react/renderer/mounting) +add_react_common_subdir(react/renderer/scheduler) +add_react_common_subdir(react/renderer/telemetry) +add_react_common_subdir(react/renderer/viewtransition) +add_react_common_subdir(react/renderer/uimanager) +add_react_common_subdir(react/renderer/bridging) +add_react_common_subdir(react/renderer/core) +add_react_common_subdir(react/renderer/consistency) +add_react_common_subdir(react/renderer/css) +add_react_common_subdir(react/renderer/uimanager/consistency) +add_react_common_subdir(react/renderer/dom) +add_react_common_subdir(react/renderer/element) +add_react_common_subdir(react/renderer/debug) +add_react_common_subdir(react/renderer/graphics) +add_react_common_subdir(react/renderer/imagemanager) +add_react_common_subdir(react/renderer/components/view) +# GPUI uses RN's shared text-input state/event implementation with the +# platform-neutral TextInput shadow node props. Do not build React Native's +# Android target here: that target glob includes JNI-only sources. +set(RN_GPUI_TEXTINPUT_DIR + "${REACT_COMMON_DIR}/react/renderer/components/textinput") +set(RN_GPUI_IOS_TEXTINPUT_DIR + "${RN_GPUI_TEXTINPUT_DIR}/platform/ios/react/renderer/components/iostextinput") +add_library(rrc_textinput_gpui OBJECT + "${RN_GPUI_TEXTINPUT_DIR}/BaseTextInputProps.cpp" + "${RN_GPUI_TEXTINPUT_DIR}/TextInputEventEmitter.cpp" + "${RN_GPUI_IOS_TEXTINPUT_DIR}/TextInputProps.cpp" + "${RN_GPUI_IOS_TEXTINPUT_DIR}/TextInputShadowNode.cpp") +target_include_directories(rrc_textinput_gpui PUBLIC + "${RN_GPUI_TEXTINPUT_DIR}" + "${RN_GPUI_TEXTINPUT_DIR}/platform/ios" + "${RN_GPUI_IOS_TEXTINPUT_DIR}") +target_link_libraries(rrc_textinput_gpui PUBLIC + react_renderer_attributedstring + react_renderer_componentregistry + react_renderer_core + react_renderer_graphics + react_renderer_mounting + react_renderer_textlayoutmanager + react_renderer_uimanager + react_utils + rrc_text + rrc_view + yoga) +target_compile_reactnative_options(rrc_textinput_gpui PRIVATE) +target_compile_options(rrc_textinput_gpui PRIVATE -Wpedantic) +# react/renderer/components/switch is Android-only (its sole +# source is the fbjni-based AndroidSwitchMeasurementsManager; +# nothing links rrc_switch and the generated FBReactNativeSpec +# code does not reference it) — not built on the gpui platform. +# react/renderer/components/textinput and +# react/renderer/components/progressbar ship only android (fbjni) +# and ios (CocoaPods) platform sources; the generated +# FBReactNativeSpec code is self-contained and nothing links +# rrc_textinput/rrc_progressbar — not built on the gpui platform. +add_react_common_subdir(react/renderer/components/root) +add_react_common_subdir(react/renderer/components/image) +add_react_common_subdir(react/renderer/components/legacyviewmanagerinterop) +add_react_common_subdir(react/renderer/componentregistry/native) +add_react_common_subdir(react/renderer/components/text) +add_react_common_subdir(react/renderer/components/unimplementedview) +add_react_common_subdir(react/renderer/components/modal) +add_react_common_subdir(react/renderer/components/scrollview) +add_react_common_subdir(react/renderer/components/safeareaview) +add_react_common_subdir(react/renderer/leakchecker) +add_react_common_subdir(react/renderer/observers/events) +add_react_common_subdir(react/renderer/observers/intersection) +add_react_common_subdir(react/renderer/observers/mutation) +add_react_common_subdir(react/renderer/textlayoutmanager) + +# Upstream's cxx `TextLayoutManager::measure` returns the Yoga minimum size, so +# every paragraph collapses to zero height. Compile our copy override +# (ReactCommonOverrides/.../TextLayoutManager.cpp) in place of the upstream +# platform/cxx source; overrides.json tracks it so upstream drift is caught by +# `react-native-platform-override validate`. +set(RN_GPUI_TEXT_LAYOUT_MANAGER_OVERRIDE + "${RN_GPUI_OVERRIDES_DIR}/react/renderer/textlayoutmanager/platform/cxx/react/renderer/textlayoutmanager/TextLayoutManager.cpp") +if(TARGET react_renderer_textlayoutmanager AND EXISTS "${RN_GPUI_TEXT_LAYOUT_MANAGER_OVERRIDE}") + get_target_property(_rn_gpui_tlm_sources react_renderer_textlayoutmanager SOURCES) + # The platform selector resolves to absolute paths, the fallback to relative + # ones: match either. CONFIGURE_DEPENDS re-globs on reconfigure, so the + # upstream entry is re-added and has to be filtered out again every time. + list(FILTER _rn_gpui_tlm_sources EXCLUDE + REGEX "textlayoutmanager/platform/cxx/.*TextLayoutManager\\.cpp$") + list(APPEND _rn_gpui_tlm_sources "${RN_GPUI_TEXT_LAYOUT_MANAGER_OVERRIDE}") + set_property(TARGET react_renderer_textlayoutmanager + PROPERTY SOURCES "${_rn_gpui_tlm_sources}") + message(STATUS "rn_gpui_core: TextLayoutManager override: ${RN_GPUI_TEXT_LAYOUT_MANAGER_OVERRIDE}") +else() + message(WARNING + "rn_gpui_core: TextLayoutManager override not found (${RN_GPUI_TEXT_LAYOUT_MANAGER_OVERRIDE}); " + "paragraphs will measure to zero height") +endif() +add_react_common_subdir(react/utils) +add_react_common_subdir(react/bridging) +add_react_common_subdir(react/renderer/mapbuffer) +add_react_common_subdir(react/nativemodule/core) +add_react_common_subdir(react/nativemodule/defaults) +add_react_common_subdir(react/nativemodule/dom) +add_react_common_subdir(react/nativemodule/featureflags) +add_react_common_subdir(react/nativemodule/microtasks) +add_react_common_subdir(react/nativemodule/idlecallbacks) +add_react_common_subdir(react/nativemodule/intersectionobserver) +add_react_common_subdir(react/nativemodule/mutationobserver) +add_react_common_subdir(react/nativemodule/viewtransition) +add_react_common_subdir(react/nativemodule/webperformance) +add_react_common_subdir(react/networking) +add_react_common_subdir(jserrorhandler) +add_react_common_subdir(react/runtime) +add_react_common_subdir(react/runtime/nativeviewconfig) +add_react_common_subdir(react/timing) + +add_react_cxx_platform_subdir(react/coremodules) +add_react_cxx_platform_subdir(react/devsupport) + +# --- DevServerHelper patch override ---------- +# +# Upstream hardcodes the Android platform string, so Metro would be asked for +# the Android JS surface. Compile the copy override that passes +# `platform=desktop` in place of the vendored source; overrides.json tracks it so +# upstream drift is caught by `react-native-platform-override validate`. +set(RN_GPUI_DEV_SERVER_HELPER_OVERRIDE + "${RN_GPUI_OVERRIDES_DIR}/react/devsupport/DevServerHelper.cpp") +if(TARGET react_cxx_platform_react_devsupport AND EXISTS "${RN_GPUI_DEV_SERVER_HELPER_OVERRIDE}") + get_target_property(_rn_gpui_dsh_sources react_cxx_platform_react_devsupport SOURCES) + # The upstream glob is CONFIGURE_DEPENDS, so the vendored entry is re-added on + # every reconfigure and has to be filtered out again. + list(FILTER _rn_gpui_dsh_sources EXCLUDE + REGEX "devsupport/DevServerHelper\\.cpp$") + list(APPEND _rn_gpui_dsh_sources "${RN_GPUI_DEV_SERVER_HELPER_OVERRIDE}") + set_property(TARGET react_cxx_platform_react_devsupport + PROPERTY SOURCES "${_rn_gpui_dsh_sources}") + message(STATUS "rn_gpui_core: DevServerHelper override: ${RN_GPUI_DEV_SERVER_HELPER_OVERRIDE}") +else() + message(WARNING + "rn_gpui_core: DevServerHelper override not found (${RN_GPUI_DEV_SERVER_HELPER_OVERRIDE}); " + "the dev server will be asked for the Android bundle") +endif() + +# NativeDOM.cpp includes "NativeDOM.h" by quoted path, which always resolves to +# the sibling upstream header, so the header override alone is not enough. The +# copy override drops the backticks in its `#pragma mark` lines (MSVC rejects +# them with C2018) and includes the header through the override include path. +set(RN_GPUI_NATIVE_DOM_OVERRIDE + "${RN_GPUI_OVERRIDES_DIR}/react/nativemodule/dom/NativeDOM.cpp") +if(TARGET react_nativemodule_dom) + get_target_property(_rn_gpui_dom_sources react_nativemodule_dom SOURCES) + list(FILTER _rn_gpui_dom_sources EXCLUDE REGEX "dom/NativeDOM\\.cpp$") + list(APPEND _rn_gpui_dom_sources "${RN_GPUI_NATIVE_DOM_OVERRIDE}") + set_property(TARGET react_nativemodule_dom + PROPERTY SOURCES "${_rn_gpui_dom_sources}") +endif() + +add_react_cxx_platform_subdir(react/http) + +# Upstream's ReactCxxPlatform/react/http/CMakeLists.txt globs only *.cpp, so it +# misses platform/cxx/WebSocketClient.cpp (the boost.beast cross-platform +# WebSocket client, which also needs ReactCommon headers). Patch the target from +# here rather than editing the vendored tree: ReactCxxPlatform is a byte-for-byte +# copy override, so a local edit would fail +# `react-native-platform-override validate`. +if(TARGET react_cxx_platform_react_http) + file(GLOB _rn_gpui_http_platform_cxx_SRC CONFIGURE_DEPENDS + "${REACT_CXX_PLATFORM_DIR}/react/http/platform/cxx/*.cpp") + target_sources(react_cxx_platform_react_http PRIVATE ${_rn_gpui_http_platform_cxx_SRC}) + target_include_directories(react_cxx_platform_react_http PUBLIC ${REACT_COMMON_DIR}) + if(APPLE) + find_library(RN_GPUI_SECURITY_FRAMEWORK Security REQUIRED) + target_link_libraries(react_cxx_platform_react_http ${RN_GPUI_SECURITY_FRAMEWORK}) + elseif(WIN32) + target_link_libraries(react_cxx_platform_react_http Crypt32) + endif() +endif() +add_react_cxx_platform_subdir(react/io) +target_include_directories(react_cxx_platform_react_io PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(react_cxx_platform_react_io OpenSSL::SSL) +add_react_cxx_platform_subdir(react/logging) +add_react_cxx_platform_subdir(react/nativemodule) +add_react_cxx_platform_subdir(react/profiling) +add_react_cxx_platform_subdir(react/renderer/scheduler) +add_react_cxx_platform_subdir(react/renderer/uimanager) +add_react_cxx_platform_subdir(react/threading) +add_react_cxx_platform_subdir(react/utils) +add_react_cxx_platform_subdir(react/runtime) +# PlatformConstantsModule reports the active engine as +# `Platform.constants.jsEngine`, so it includes GpuiJsEngine.h from here. +target_include_directories(react_cxx_platform_react_coremodules PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}") + +enable_testing() +add_executable(gpui_js_engine_test + test/gpui_js_engine_test.cpp + GpuiJsEngine.cpp) +target_include_directories(gpui_js_engine_test PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${REACT_COMMON_DIR}") +target_link_libraries(gpui_js_engine_test PRIVATE jsi) +add_test(NAME gpui_js_engine_selection COMMAND gpui_js_engine_test) + +# GpuiTurboModule.cpp logs through glog, which a prebuilt configure only has as +# an imported target when it is in the runtime's link closure. +if(TARGET jsi AND TARGET glog) + add_executable(gpui_turbo_module_test + test/gpui_turbo_module_test.cpp + GpuiTurboModule.cpp) + target_include_directories(gpui_turbo_module_test PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${REACT_COMMON_DIR}" + "${REACT_COMMON_DIR}/callinvoker" + "${REACT_COMMON_DIR}/react/nativemodule/core" + "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}") + target_link_libraries(gpui_turbo_module_test PRIVATE jsi glog) + add_test(NAME gpui_turbo_module_queue COMMAND gpui_turbo_module_test) +endif() + +# The engine packages' own smoke tests (for example a VM smoke test) are +# added by their desktop/CMakeLists.txt, which autolink includes below. + +add_library(rn_gpui_core STATIC + GpuiDynamic.cpp + # The Rust TurboModule provider is registered at run time, so it is always + # part of the platform: it looks a module up by name when JS asks for one. + GpuiRustTurboModules.cpp + GpuiHost.cpp + GpuiHttpClient.cpp + GpuiHttpBridge.cpp + GpuiStartupSignpost.cpp + GpuiJsEngine.cpp + GpuiLogBoxSurfaceDelegate.cpp + GpuiMountingManager.cpp + # The C++ half of the host component API: one generic props class, one + # shadow node with a state-driven measure function, and a descriptor per + # registered component name. + GpuiHostComponent.cpp + # Stock Node-API addon loading: the `_requireNodeLibrary` JSI global, path + # resolution, the cache and the error messages. Compiled on every platform — + # it is one small source file with no extra library — and it talks to the VM + # only through the active engine's NodeAddons capability. The Windows half + # (`GetModuleFileNameW` and `;` as the list separator) is live there now that + # the app's executable forwards the VM's Node-API exports. + GpuiNodeAddons.cpp + GpuiTurboModule.cpp + MountBatchBuilder.cpp + # A built-in component's props extraction. It registers by component name, so + # the mounting manager names no component. + builtins/image/GpuiImageProps.cpp + builtins/modal/GpuiModalProps.cpp + builtins/scrollview/GpuiScrollViewProps.cpp + builtins/textinput/GpuiTextInputProps.cpp + GpuiModules.cpp) +if(APPLE) + set_source_files_properties(GpuiAccessibilityMac.mm PROPERTIES + COMPILE_FLAGS "-fobjc-arc") + target_sources(rn_gpui_core PRIVATE GpuiAccessibilityMac.mm) + target_link_libraries(rn_gpui_core PUBLIC "-framework AppKit") +endif() +target_include_directories(rn_gpui_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(rn_gpui_core PUBLIC "${REACT_CXX_PLATFORM_DIR}") +target_sources(rn_gpui_core PRIVATE + "${REACT_CXX_PLATFORM_DIR}/react/renderer/components/nativeviewhost/NativeViewHost.cpp" + "${REACT_CXX_PLATFORM_DIR}/react/renderer/components/windowcontrol/WindowControl.cpp" + "${REACT_CXX_PLATFORM_DIR}/react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.cpp" + "${REACT_CXX_PLATFORM_DIR}/react/renderer/components/shortcutscope/ShortcutScope.cpp" + builtins/window_control/GpuiWindowControlProps.cpp + builtins/context_menu_view/GpuiContextMenuViewProps.cpp + builtins/shortcut_scope/GpuiShortcutScopeProps.cpp) +if(RN_GPUI_AUTOLINK_LIBRARIES) + target_link_libraries(rn_gpui_core PUBLIC ${RN_GPUI_AUTOLINK_LIBRARIES}) +endif() + +# The cxx-generated bridge header is located above, +# before the ReactCommon subdirectories. rn_gpui_core consumes it directly. +if(RN_GPUI_CXXBRIDGE_INCLUDE_DIR AND EXISTS "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}/rust/cxx.h") + target_include_directories(rn_gpui_core PUBLIC "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}") +else() + message(WARNING + "rn_gpui_core: cxx bridge headers not found — run `cargo build -p rn-gpui-sys` first, " + "or pass -DRN_GPUI_CXXBRIDGE_INCLUDE_DIR=<.../out/cxxbridge/include>") +endif() + +# The glue includes headers from many RN libraries, so give it the union of +# their interface include directories. OBJECT libraries forward those to +# consumers, and their objects reach the final link through the consuming +# binary — which is exactly how the Rust cdylib will consume this static +# library. Targets listed in Fantom's tester build that do not +# exist here are guard-skipped with a warning rather than dropped silently. +set(RN_GPUI_CORE_DEPS + CURL::libcurl + jsi + jserrorhandler + jsitooling + oscompat + jsinspector + # jsinspector is an OBJECT library; a dependency on the parent does not + # archive the children's objects, so they are named explicitly (otherwise + # cdp::jsonRequest/NetworkHandler stay undefined). An engine package names its + # own OBJECT libraries the same way in RN_GPUI_JS_ENGINE_DEPS. + jsinspector_cdp + jsinspector_network + jsinspector_tracing + logger + react_bridging + react_codegen_rncore + react_cxxreact + react_debug + react_featureflags + react_nativemodule_core + react_nativemodule_defaults + react_nativemodule_dom + react_nativemodule_featureflags + react_nativemodule_idlecallbacks + react_nativemodule_intersectionobserver + react_nativemodule_microtasks + react_nativemodule_mutationobserver + react_nativemodule_viewtransition + react_nativemodule_webperformance + react_networking + react_performance_cdpmetrics + react_performance_timeline + react_utils + reactperflogger + bridgeless + react_renderer_animated + react_renderer_animationbackend + react_renderer_animations + react_renderer_attributedstring + react_renderer_componentregistry + react_renderer_consistency + react_renderer_core + react_renderer_css + react_renderer_debug + react_renderer_dom + react_renderer_element + react_renderer_graphics + react_renderer_imagemanager + react_renderer_leakchecker + react_renderer_mapbuffer + react_renderer_mounting + react_renderer_observers_events + react_renderer_observers_intersection + react_renderer_observers_mutation + react_renderer_runtimescheduler + react_renderer_scheduler + react_renderer_telemetry + react_renderer_textlayoutmanager + react_renderer_uimanager + react_renderer_uimanager_consistency + react_renderer_viewtransition + rrc_image + rrc_legacyviewmanagerinterop + rrc_modal + rrc_native + rrc_root + rrc_safeareaview + rrc_scrollview + rrc_text + rrc_textinput_gpui + rrc_unimplementedview + rrc_view + yoga + react_cxx_platform_react_coremodules + react_cxx_platform_react_devsupport + react_cxx_platform_react_http + react_cxx_platform_react_io + react_cxx_platform_react_logging + react_cxx_platform_react_nativemodule + react_cxx_platform_react_profiling + react_cxx_platform_react_renderer_scheduler + react_cxx_platform_react_renderer_uimanager + react_cxx_platform_react_runtime + react_cxx_platform_react_threading + react_cxx_platform_react_utils) +foreach(dep IN LISTS RN_GPUI_CORE_DEPS) + if(TARGET ${dep}) + target_link_libraries(rn_gpui_core PUBLIC ${dep}) + else() + message(WARNING "rn_gpui_core: target '${dep}' does not exist in this build") + endif() +endforeach() + +endif() # NOT RN_GPUI_PREBUILT_DIR (source build of rn_gpui_core and its deps) + +# Autolinked app packages are needed in both modes: they only add sources to +# global properties and app-side targets. +if(RN_GPUI_AUTOLINK_CMAKE AND EXISTS "${RN_GPUI_AUTOLINK_CMAKE}") + include("${RN_GPUI_AUTOLINK_CMAKE}") +endif() + +# JS engine packages (GpuiJsEngine.h) declare the React Native glue they need and +# the targets the app must link. The glue is added here, in core's scope, so its +# targets can be linked into rn_gpui_core (source builds archive their objects). +get_property(RN_GPUI_JS_ENGINE_SUBDIRECTORIES GLOBAL PROPERTY RN_GPUI_JS_ENGINE_SUBDIRECTORIES) +foreach(_rn_gpui_engine_subdir IN LISTS RN_GPUI_JS_ENGINE_SUBDIRECTORIES) + add_react_common_subdir(${_rn_gpui_engine_subdir}) +endforeach() + +# With a prebuilt runtime rn_gpui_core is imported, so the engine targets go to +# rn_gpui_modules instead (see below). In a source build they join +# RN_GPUI_CORE_DEPS so every consumer of core's dependency list — the app archive +# and the modules archive — also sees the engine's include directories. +get_property(RN_GPUI_JS_ENGINE_DEPS GLOBAL PROPERTY RN_GPUI_JS_ENGINE_DEPS) +if(RN_GPUI_JS_ENGINE_DEPS AND NOT RN_GPUI_PREBUILT_DIR) + list(APPEND RN_GPUI_CORE_DEPS ${RN_GPUI_JS_ENGINE_DEPS}) + target_link_libraries(rn_gpui_core PUBLIC ${RN_GPUI_JS_ENGINE_DEPS}) +endif() + +# App TurboModules are generated under desktop/autolink and linked as a +# separate archive. The platform runtime only owns the registration hook. +if(RN_GPUI_APP_CPP_PROVIDER_SOURCE AND EXISTS "${RN_GPUI_APP_CPP_PROVIDER_SOURCE}") + get_property(RN_GPUI_CMAKE_APP_CPP_SOURCES GLOBAL PROPERTY RN_GPUI_APP_CPP_SOURCES) + get_property(RN_GPUI_CMAKE_APP_CPP_INCLUDE_DIRS GLOBAL PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS) + get_property(RN_GPUI_CMAKE_APP_CPP_DEFINITIONS GLOBAL PROPERTY RN_GPUI_APP_CPP_DEFINITIONS) + get_property(RN_GPUI_CMAKE_APP_CPP_COMPILE_OPTIONS GLOBAL PROPERTY RN_GPUI_APP_CPP_COMPILE_OPTIONS) + list(APPEND RN_GPUI_APP_CPP_SOURCES ${RN_GPUI_CMAKE_APP_CPP_SOURCES}) + add_library(rn_gpui_app_turbo_modules STATIC + "${RN_GPUI_APP_CPP_PROVIDER_SOURCE}" + ${RN_GPUI_APP_CPP_SOURCES}) + target_include_directories(rn_gpui_app_turbo_modules PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}" + ${RN_GPUI_CMAKE_APP_CPP_INCLUDE_DIRS}) + target_compile_definitions(rn_gpui_app_turbo_modules PRIVATE ${RN_GPUI_CMAKE_APP_CPP_DEFINITIONS}) + target_compile_options(rn_gpui_app_turbo_modules PRIVATE ${RN_GPUI_CMAKE_APP_CPP_COMPILE_OPTIONS}) + if(RN_GPUI_PREBUILT_DIR) + # The imported rn_gpui_core already carries ReactCommon, folly and the rest; + # it cannot be given a link to this archive, so the link order comes from + # link_libs-.txt instead (see the link_libs section). + target_link_libraries(rn_gpui_app_turbo_modules PRIVATE rn_gpui_core) + # App sources may use the active engine's headers (a package that binds the + # Hermes runtime needs them), which the archive does not ship. The engine + # targets carry them. + if(RN_GPUI_JS_ENGINE_DEPS) + target_link_libraries(rn_gpui_app_turbo_modules PRIVATE ${RN_GPUI_JS_ENGINE_DEPS}) + endif() + else() + target_link_libraries(rn_gpui_app_turbo_modules PRIVATE ${RN_GPUI_CORE_DEPS}) + target_link_libraries(rn_gpui_core PUBLIC rn_gpui_app_turbo_modules) + endif() +endif() + +# --- rn_gpui_modules: optional library glue (cpp/GpuiModules.h) ------- +# +# Glue for an optional package is compiled here, in the app's build, rather than +# into rn_gpui_core, so rn_gpui_core compiles the same files with the same flags +# for every app. The package that needs the glue ships it in its own `gpui/cpp/` +# and declares it through the `RN_GPUI_PACKAGE_MODULE_*` properties below; +# what stays here is the platform's own glue, which no package owns. +# rn_gpui_install_modules() (generated below) calls each installer once, and +# GpuiHost calls it from its constructor. +set(RN_GPUI_MODULE_SOURCES) +set(RN_GPUI_MODULE_INSTALLERS) +set(RN_GPUI_MODULE_INCLUDE_DIRS) +set(RN_GPUI_MODULE_DEFINITIONS) + +get_property(RN_GPUI_PACKAGE_MODULE_SOURCES GLOBAL PROPERTY RN_GPUI_PACKAGE_MODULE_SOURCES) +get_property(RN_GPUI_PACKAGE_MODULE_INSTALLERS GLOBAL PROPERTY RN_GPUI_PACKAGE_MODULE_INSTALLERS) +get_property(RN_GPUI_PACKAGE_MODULE_INCLUDE_DIRS GLOBAL PROPERTY RN_GPUI_PACKAGE_MODULE_INCLUDE_DIRS) +list(APPEND RN_GPUI_MODULE_SOURCES ${RN_GPUI_PACKAGE_MODULE_SOURCES}) +list(APPEND RN_GPUI_MODULE_INSTALLERS ${RN_GPUI_PACKAGE_MODULE_INSTALLERS}) +list(APPEND RN_GPUI_MODULE_INCLUDE_DIRS ${RN_GPUI_PACKAGE_MODULE_INCLUDE_DIRS}) +# A module reads its library's Fabric props, which the app's codegen produced. +get_property(RN_GPUI_APP_CPP_INCLUDE_DIRS GLOBAL PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS) +list(APPEND RN_GPUI_MODULE_INCLUDE_DIRS ${RN_GPUI_APP_CPP_INCLUDE_DIRS}) + +# JS engine packages (GpuiJsEngine.h): the registration shim is compiled into +# this archive, and the generated installer below calls its symbol before the +# other installers so the registry is populated before the host selects an +# engine. +get_property(RN_GPUI_JS_ENGINE_SOURCES GLOBAL PROPERTY RN_GPUI_JS_ENGINE_SOURCES) +get_property(RN_GPUI_JS_ENGINE_INCLUDE_DIRS GLOBAL PROPERTY RN_GPUI_JS_ENGINE_INCLUDE_DIRS) +get_property(RN_GPUI_JS_ENGINE_DEFINITIONS GLOBAL PROPERTY RN_GPUI_JS_ENGINE_DEFINITIONS) +get_property(RN_GPUI_JS_ENGINE_REGISTRATIONS GLOBAL PROPERTY RN_GPUI_JS_ENGINE_REGISTRATIONS) +list(APPEND RN_GPUI_MODULE_SOURCES ${RN_GPUI_JS_ENGINE_SOURCES}) +list(APPEND RN_GPUI_MODULE_INCLUDE_DIRS ${RN_GPUI_JS_ENGINE_INCLUDE_DIRS}) +list(APPEND RN_GPUI_MODULE_DEFINITIONS ${RN_GPUI_JS_ENGINE_DEFINITIONS}) + +# Core's own built-in modules register their renderers through the same +# generated installer, so a built-in is wired exactly like a package module. +list(APPEND RN_GPUI_MODULE_INSTALLERS context_menu_view image modal scroll_view shortcut_scope text_input window_control) + +# The generated installer list depends on which packages are present, so it is +# written at configure time. file(CONFIGURE) only rewrites it when it changes. +set(RN_GPUI_MODULE_DECLARATIONS "") +set(RN_GPUI_MODULE_CALLS "") +foreach(_rn_gpui_installer IN LISTS RN_GPUI_MODULE_INSTALLERS) + string(APPEND RN_GPUI_MODULE_DECLARATIONS "void rn_gpui_install_${_rn_gpui_installer}_module();\n") + string(APPEND RN_GPUI_MODULE_CALLS " rn_gpui_install_${_rn_gpui_installer}_module();\n") +endforeach() +set(RN_GPUI_ENGINE_DECLARATIONS "") +set(RN_GPUI_ENGINE_CALLS "") +foreach(_rn_gpui_engine IN LISTS RN_GPUI_JS_ENGINE_REGISTRATIONS) + string(APPEND RN_GPUI_ENGINE_DECLARATIONS "extern \"C\" void ${_rn_gpui_engine}();\n") + string(APPEND RN_GPUI_ENGINE_CALLS " ${_rn_gpui_engine}();\n") +endforeach() +set(RN_GPUI_INSTALL_MODULES_CPP "${CMAKE_BINARY_DIR}/generated/rn_gpui_install_modules.cpp") +file(CONFIGURE OUTPUT "${RN_GPUI_INSTALL_MODULES_CPP}" CONTENT [=[ +// Generated by rn-gpui-sys cpp/CMakeLists.txt. Do not edit. +#include "GpuiModules.h" + +#include + +namespace rngpui { +@RN_GPUI_ENGINE_DECLARATIONS@ +@RN_GPUI_MODULE_DECLARATIONS@ +void rn_gpui_install_modules() { + static std::once_flag installed; + std::call_once(installed, [] { + // Engine packages first: the host selects an engine from the registry right + // after this call, and module installers may also register one. +@RN_GPUI_ENGINE_CALLS@@RN_GPUI_MODULE_CALLS@ }); +} + +} // namespace rngpui +]=]) + +# Always defined, even when no package is present, so link_libs.txt is stable. +add_library(rn_gpui_modules STATIC + "${RN_GPUI_INSTALL_MODULES_CPP}" + ${RN_GPUI_MODULE_SOURCES}) +target_compile_definitions(rn_gpui_modules PRIVATE ${RN_GPUI_MODULE_DEFINITIONS}) +target_include_directories(rn_gpui_modules PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${REACT_CXX_PLATFORM_DIR}" + ${RN_GPUI_MODULE_INCLUDE_DIRS}) +if(RN_GPUI_CXXBRIDGE_INCLUDE_DIR AND EXISTS "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}/rust/cxx.h") + target_include_directories(rn_gpui_modules PRIVATE "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}") +endif() +target_link_libraries(rn_gpui_modules PRIVATE ${RN_GPUI_CORE_DEPS}) +target_link_libraries(rn_gpui_modules PUBLIC rn_gpui_core) +# The app archive is not linked into rn_gpui_core in prebuilt mode, so make the +# build produce it explicitly; link_libs-.txt still references it. +if(RN_GPUI_PREBUILT_DIR AND TARGET rn_gpui_app_turbo_modules) + add_dependencies(rn_gpui_modules rn_gpui_app_turbo_modules) +endif() +# Out-of-tree addons can require native build targets without linking into +# core. They hang off this archive because it is built in both modes; an +# imported rn_gpui_core cannot carry build dependencies. +get_property(RN_GPUI_APP_NATIVE_TARGETS GLOBAL PROPERTY RN_GPUI_APP_NATIVE_TARGETS) +foreach(_rn_gpui_native_target IN LISTS RN_GPUI_APP_NATIVE_TARGETS) + add_dependencies(rn_gpui_modules ${_rn_gpui_native_target}) +endforeach() + +# With a prebuilt runtime rn_gpui_core is imported, so the engine glue targets +# cannot be linked into it; they are archived into this archive instead. The +# registration shim lives here too, so the objects reach the executable through +# the archive the app already links. +if(RN_GPUI_PREBUILT_DIR AND RN_GPUI_JS_ENGINE_DEPS) + target_link_libraries(rn_gpui_modules PRIVATE ${RN_GPUI_JS_ENGINE_DEPS}) +endif() + +# --- link_libs.txt for build.rs -------------------------- +# +# Cargo propagates `rustc-link-search` / `rustc-link-lib` from a build script +# that declares `links = "..."` to every dependent crate, whereas +# `cargo:rustc-link-arg` would not leave an rlib. The list is therefore written +# as linker file *names* plus their directories, never as absolute paths. On +# Windows the linker file for a shared target is its import library, while its +# runtime target file is the DLL; TARGET_LINKER_FILE selects the right artifact. +# +# The objects of every OBJECT library linked into rn_gpui_core are already +# archived inside librn_gpui_core.a, so only real archives and dylibs are listed. + +function(rn_gpui_collect_link_targets root out_var) + set(_queue "${root}") + set(_seen "") + while(_queue) + list(POP_FRONT _queue _entry) + if(_entry IN_LIST _seen) + continue() + endif() + if(NOT TARGET "${_entry}") + continue() + endif() + get_target_property(_type "${_entry}" TYPE) + if(NOT _type MATCHES "LIBRARY$") + continue() + endif() + list(APPEND _seen "${_entry}") + get_target_property(_links "${_entry}" LINK_LIBRARIES) + get_target_property(_interface_links "${_entry}" INTERFACE_LINK_LIBRARIES) + foreach(_more IN LISTS _links _interface_links) + if(TARGET "${_more}" AND NOT _more IN_LIST _seen) + list(APPEND _queue "${_more}") + endif() + endforeach() + endwhile() + set(${out_var} "${_seen}" PARENT_SCOPE) +endfunction() + +rn_gpui_collect_link_targets(rn_gpui_core RN_GPUI_LINK_TARGETS) +# An imported rn_gpui_core only names the archive's own libraries in its +# interface, so the active engine's shared libraries (its VM) are added to the +# roots the linker directives are collected from. Their OBJECT libraries are +# skipped by the loop below; those objects are archived into rn_gpui_modules. +if(RN_GPUI_PREBUILT_DIR AND RN_GPUI_JS_ENGINE_DEPS) + list(APPEND RN_GPUI_LINK_TARGETS ${RN_GPUI_JS_ENGINE_DEPS}) +endif() + +if(NOT RN_GPUI_PREBUILT_DIR) + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/PrebuiltInstall.cmake) +endif() + +set(RN_GPUI_LINK_LIBS "") +# Directories that must end up on the executable's rpath: Rust does not turn +# `rustc-link-search` into an rpath. build.rs forwards this list to rn-gpui, +# whose build script emits the platform-specific linker flags. +set(RN_GPUI_RPATH_DIRS "") + +# Shared libraries an engine package adds (its VM, for example) are ordinary +# imported targets: the loop below finds them by walking rn_gpui_core's link +# interfaces and emits both the linker directive and the rpath. No engine is +# special-cased here. +# +# rn_gpui_modules appears before and after rn_gpui_core (see below). +string(APPEND RN_GPUI_LINK_LIBS "search:$\nname:$\n") +foreach(_target IN LISTS RN_GPUI_LINK_TARGETS) + get_target_property(_type "${_target}" TYPE) + get_target_property(_is_framework "${_target}" FRAMEWORK) + if(_is_framework) + get_target_property(_framework_output_name "${_target}" OUTPUT_NAME) + if(NOT _framework_output_name) + set(_framework_output_name "${_target}") + endif() + get_target_property(_framework_search_dir "${_target}" LIBRARY_OUTPUT_DIRECTORY) + string(APPEND RN_GPUI_LINK_LIBS "framework-search:${_framework_search_dir}\nframework-name:${_framework_output_name}\n") + string(APPEND RN_GPUI_RPATH_DIRS "${_framework_search_dir}\n") + continue() + endif() + # UNKNOWN_LIBRARY covers imported libraries found by find_package() (e.g. + # OpenSSL::Crypto from FindOpenSSL); they still have a usable TARGET_FILE. + if(_type STREQUAL "STATIC_LIBRARY" OR _type STREQUAL "SHARED_LIBRARY" OR _type STREQUAL "UNKNOWN_LIBRARY") + string(APPEND RN_GPUI_LINK_LIBS "search:$\nname:$\n") + if(_type STREQUAL "SHARED_LIBRARY" OR _type STREQUAL "UNKNOWN_LIBRARY") + string(APPEND RN_GPUI_RPATH_DIRS "$\n") + endif() + endif() + # Static libraries hide their link-only dependencies behind generator + # expressions, so rn_gpui_collect_link_targets cannot discover this archive + # by walking rn_gpui_core's interface links. Put it directly after the core + # archive and before the ReactCommon archives it depends on. + if(_target STREQUAL "rn_gpui_core" AND TARGET rn_gpui_app_turbo_modules) + string(APPEND RN_GPUI_LINK_LIBS "search:$\nname:$\n") + endif() + # Installers read rn_gpui_core's hook storage and call into the app archive, + # so the modules archive comes after both (static archives resolve left + # to right). + if(_target STREQUAL "rn_gpui_core" AND TARGET rn_gpui_modules) + string(APPEND RN_GPUI_LINK_LIBS "search:$\nname:$\n") + endif() +endforeach() + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + # libstdc++ emits these helpers for lock-free 16-byte atomics on x86_64. + # Place it after the static RN archives so the final Rust link resolves them. + string(APPEND RN_GPUI_LINK_LIBS "name:atomic\n") +endif() + +# The content names target file directories, which differ per configuration on +# multi-config generators (Visual Studio), so file(GENERATE) requires the +# configuration in the file name. build.rs reads the one matching its profile. +file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/link_libs-$.txt" CONTENT "${RN_GPUI_LINK_LIBS}") +file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/rpath_dirs-$.txt" CONTENT "${RN_GPUI_RPATH_DIRS}") + +# --- MSVC compile-flag cleanup ------------------------------------------- +# React Native's shared flag list (cmake-utils/react-native-flags.cmake) is +# written for clang/gcc and is applied to every ReactCommon target as +# -Wall -Werror -fexceptions -frtti -std=c++20, and the bundled third-party +# projects add more of the same (-Wwrite-strings, -Wno-unused-variable, +# -Wpedantic). cl.exe reads those as /W options with non-numeric arguments and +# aborts the build with, for example, +# cl : command line error D8021: invalid numeric argument '/Werror' +# Every RN subdirectory includes that file itself, so the list cannot be +# overridden from this file; the GCC-only spellings are removed from the +# targets once they all exist, and the MSVC equivalents are added instead. +if(MSVC) + function(rn_gpui_strip_gcc_only_flags directory) + get_property(_targets DIRECTORY "${directory}" PROPERTY BUILDSYSTEM_TARGETS) + foreach(_target IN LISTS _targets) + foreach(_property IN ITEMS COMPILE_OPTIONS INTERFACE_COMPILE_OPTIONS) + get_target_property(_options "${_target}" "${_property}") + if(NOT _options) + continue() + endif() + set(_kept "") + set(_stripped FALSE) + foreach(_option IN LISTS _options) + # -W... is a clang/gcc warning flag (MSVC spells its own /W0-/W4, + # /Wall and /WX, which do not match this pattern); -f..., -std=..., + # -m... and -pthread have MSVC equivalents or are meaningless there. + if(_option MATCHES "^-W[a-zA-Z]" OR _option MATCHES "^-f" + OR _option MATCHES "^-std=" OR _option MATCHES "^-m[a-z]" + OR _option MATCHES "^-pthread" + # RN's glog CMakeLists hardcodes -DHAVE_PREAD=1 for Android. + OR _option MATCHES "^-DHAVE_PREAD") + set(_stripped TRUE) + else() + list(APPEND _kept "${_option}") + endif() + endforeach() + if(_stripped) + set_target_properties("${_target}" PROPERTIES "${_property}" "${_kept}") + endif() + endforeach() + + # ReactCommon components glob their per-platform sources alongside the + # portable ones; the ios/android/apple trees only compile for those + # toolchains (our platform implementation lives in platform/cxx). + # rrc_textinput_gpui compiles RN's iostextinput shadow node and props on + # purpose (see its definition), so it keeps its platform/ios sources. + get_target_property(_of_sources "${_target}" SOURCES) + if(_of_sources AND NOT _target STREQUAL "rrc_textinput_gpui") + list(FILTER _of_sources EXCLUDE REGEX "/platform/(ios|android|apple)/") + set_target_properties("${_target}" PROPERTIES SOURCES "${_of_sources}") + endif() + endforeach() + get_property(_subdirectories DIRECTORY "${directory}" PROPERTY SUBDIRECTORIES) + foreach(_subdirectory IN LISTS _subdirectories) + rn_gpui_strip_gcc_only_flags("${_subdirectory}") + endforeach() + endfunction() + + rn_gpui_strip_gcc_only_flags("${CMAKE_CURRENT_BINARY_DIR}") + + # A unity build is what makes the build fast (each target's shared headers are + # parsed once instead of once per source) and also what makes it unsafe for + # code that was never compiled that way: merging sources makes file-scope + # names and macros from one source visible to the next. The vendored + # third-party trees are exactly that — curl defines same-named file-local + # helpers in several sources, and folly's portability shims pick one + # implementation per file — so they keep the translation-unit layout their + # upstreams expect, and the win comes from ReactCommon and the gpui platform + # targets that this repository builds and tests. + # + # Selection is by source directory, not by target name: the vendored trees + # define hundreds of targets whose names differ per version, and a name list + # silently stops matching when one is renamed. + # + # `yoga` lives under ReactCommon but is a vendored upstream of its own whose + # Node.cpp cannot be merged with its siblings ("error C2872: 'Node': ambiguous + # symbol"), so both roots are matched. + # + # `rrc_view` and `rrc_legacyviewmanagerinterop` are ReactCommon targets that + # also cannot be merged, for two different upstream reasons: + # + # * `rrc_view` defines a `toString` overload per props class (Filter.h, + # BackgroundImage.h) while DebugStringConvertible.h's + # `toString(std::vector)` template is instantiated in the same unit. + # Resolution depends on which source precedes which, and merging puts + # `toString(BackgroundImage)` after the instantiation ("error C2665: no + # overloaded function could convert all the argument types"). + # * `rrc_legacyviewmanagerinterop` includes + # LegacyViewManagerInteropViewProps.h, which has no include guard at all — + # it is wrapped in `#ifndef RCT_REMOVE_LEGACY_COMPONENT_INTEROP`, a build + # feature rather than a guard — so merging redefines its classes ("error + # C2011: 'LegacyViewManagerInteropViewProps': 'class' type redefinition"). + # + # Both are ~4 sources each, so leaving them unmerged costs almost nothing and + # needs no edit to vendored React Native. + # + # OpenSSL is not a CMake target at all (it builds through its own Configure + + # Make), so it is unaffected. + function(rn_gpui_disable_unity_for_vendored directory) + get_property(_targets DIRECTORY "${directory}" PROPERTY BUILDSYSTEM_TARGETS) + foreach(_target IN LISTS _targets) + # Aliases (double_conversion, yoga, ...) reject set_target_properties, and + # the real target they name is visited by this same walk. + get_target_property(_aliased "${_target}" ALIASED_TARGET) + if(_aliased) + continue() + endif() + if(_target STREQUAL "rrc_view" + OR _target STREQUAL "rrc_legacyviewmanagerinterop" + OR _target STREQUAL "react_renderer_mounting") + set_target_properties("${_target}" PROPERTIES UNITY_BUILD OFF) + continue() + endif() + get_target_property(_source_dir "${_target}" SOURCE_DIR) + if(_source_dir AND (_source_dir MATCHES "native-deps" + OR _source_dir MATCHES "/ReactCommon/yoga/")) + set_target_properties("${_target}" PROPERTIES UNITY_BUILD OFF) + endif() + endforeach() + get_property(_subdirectories DIRECTORY "${directory}" PROPERTY SUBDIRECTORIES) + foreach(_subdirectory IN LISTS _subdirectories) + rn_gpui_disable_unity_for_vendored("${_subdirectory}") + endforeach() + endfunction() + + if(RN_GPUI_UNITY_BUILD) + rn_gpui_disable_unity_for_vendored("${CMAKE_CURRENT_BINARY_DIR}") + endif() + + # RN's jsi CMakeLists globs every jsi/*.cpp, which drags in the POSIX memory + # mapping implementation; Windows has jsilib-windows.cpp instead. + if(TARGET jsi) + get_target_property(_rn_gpui_jsi_sources jsi SOURCES) + if(_rn_gpui_jsi_sources) + list(FILTER _rn_gpui_jsi_sources EXCLUDE REGEX "jsilib-posix\\.cpp$") + set_target_properties(jsi PROPERTIES SOURCES "${_rn_gpui_jsi_sources}") + endif() + # jsi is a shared library, but JSI_EXPORT is empty above, so nothing is + # marked dllexport. Without exports link.exe writes no jsi.lib and the + # final Rust link fails with LNK1181. Export every jsi symbol instead. + set_target_properties(jsi PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) + endif() +endif() diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiAccessibilityMac.mm b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiAccessibilityMac.mm new file mode 100644 index 0000000000..90b10ef0ce --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiAccessibilityMac.mm @@ -0,0 +1,167 @@ +#import +#import + +extern "C" void rn_gpui_accessibility_setting_changed( + const char *eventName, + bool enabled); + +static id accessibilityOptionsObserver = nil; +static bool lastReduceMotion = false; +static bool lastReduceTransparency = false; +static bool lastIncreaseContrast = false; +static bool lastInvertColors = false; + +@interface GpuiVoiceOverObserver : NSObject +@end + +@implementation GpuiVoiceOverObserver +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if ([keyPath isEqualToString:@"voiceOverEnabled"]) { + const bool enabled = NSWorkspace.sharedWorkspace.isVoiceOverEnabled; + dispatch_async(dispatch_get_main_queue(), ^{ + rn_gpui_accessibility_setting_changed("screenReaderChanged", enabled); + }); + } else { + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; + } +} +@end + +static GpuiVoiceOverObserver *voiceOverObserver = nil; + +extern "C" bool rn_gpui_get_voice_over() +{ + return NSWorkspace.sharedWorkspace.isVoiceOverEnabled; +} + +extern "C" bool rn_gpui_get_invert_colors() +{ + return NSWorkspace.sharedWorkspace.accessibilityDisplayShouldInvertColors; +} + +extern "C" bool rn_gpui_get_reduce_motion() +{ + return NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceMotion; +} + +extern "C" bool rn_gpui_get_reduce_transparency() +{ + return NSWorkspace.sharedWorkspace + .accessibilityDisplayShouldReduceTransparency; +} + +extern "C" bool rn_gpui_get_increase_contrast() +{ + return NSWorkspace.sharedWorkspace.accessibilityDisplayShouldIncreaseContrast; +} + +extern "C" void rn_gpui_start_accessibility_observing() +{ + dispatch_block_t start = ^{ + if (accessibilityOptionsObserver != nil) { + return; + } + NSWorkspace *workspace = NSWorkspace.sharedWorkspace; + lastReduceMotion = workspace.accessibilityDisplayShouldReduceMotion; + lastReduceTransparency = + workspace.accessibilityDisplayShouldReduceTransparency; + lastIncreaseContrast = workspace.accessibilityDisplayShouldIncreaseContrast; + lastInvertColors = workspace.accessibilityDisplayShouldInvertColors; + voiceOverObserver = [GpuiVoiceOverObserver new]; + [workspace addObserver:voiceOverObserver + forKeyPath:@"voiceOverEnabled" + options:NSKeyValueObservingOptionNew + context:nullptr]; + accessibilityOptionsObserver = [workspace.notificationCenter + addObserverForName: + NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(__unused NSNotification *notification) { + NSWorkspace *workspace = NSWorkspace.sharedWorkspace; + const bool reduceMotion = + workspace.accessibilityDisplayShouldReduceMotion; + const bool reduceTransparency = + workspace.accessibilityDisplayShouldReduceTransparency; + const bool increaseContrast = + workspace.accessibilityDisplayShouldIncreaseContrast; + const bool invertColors = + workspace.accessibilityDisplayShouldInvertColors; + if (invertColors != lastInvertColors) { + lastInvertColors = invertColors; + rn_gpui_accessibility_setting_changed( + "invertColorsChanged", invertColors); + } + if (reduceMotion != lastReduceMotion) { + lastReduceMotion = reduceMotion; + rn_gpui_accessibility_setting_changed( + "reduceMotionChanged", reduceMotion); + } + if (reduceTransparency != lastReduceTransparency) { + lastReduceTransparency = reduceTransparency; + rn_gpui_accessibility_setting_changed( + "reduceTransparencyChanged", reduceTransparency); + } + if (increaseContrast != lastIncreaseContrast) { + lastIncreaseContrast = increaseContrast; + rn_gpui_accessibility_setting_changed( + "darkerSystemColorsChanged", increaseContrast); + } + }]; + }; + if (NSThread.isMainThread) { + start(); + } else { + dispatch_sync(dispatch_get_main_queue(), start); + } +} + +extern "C" void rn_gpui_stop_accessibility_observing() +{ + dispatch_block_t stop = ^{ + if (accessibilityOptionsObserver != nil) { + [NSWorkspace.sharedWorkspace.notificationCenter + removeObserver:accessibilityOptionsObserver]; + accessibilityOptionsObserver = nil; + [NSWorkspace.sharedWorkspace removeObserver:voiceOverObserver + forKeyPath:@"voiceOverEnabled"]; + voiceOverObserver = nil; + } + }; + if (NSThread.isMainThread) { + stop(); + } else { + dispatch_sync(dispatch_get_main_queue(), stop); + } +} + +extern "C" void rn_gpui_announce_accessibility( + const char *message, + int priorityLevel) +{ + if (message == nullptr || message[0] == '\0') { + return; + } + NSString *announcement = [[NSString alloc] initWithUTF8String:message]; + if (announcement == nil) { + return; + } + const auto priority = priorityLevel == 2 ? NSAccessibilityPriorityHigh + : priorityLevel == 0 ? NSAccessibilityPriorityLow + : NSAccessibilityPriorityMedium; + NSDictionary *info = @{ + NSAccessibilityAnnouncementKey : announcement, + NSAccessibilityPriorityKey : @(priority), + }; + dispatch_async(dispatch_get_main_queue(), ^{ + NSAccessibilityPostNotificationWithUserInfo( + NSApp, NSAccessibilityAnnouncementRequestedNotification, info); + }); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.cpp new file mode 100644 index 0000000000..f5b38c1177 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.cpp @@ -0,0 +1,196 @@ +#include "GpuiDynamic.h" + +#include +#include +#include +#include +#include + +namespace rngpui { + +namespace { + +/// One node of a value: scalars, or children with the key that names them in an +/// object. +struct Node { + uint8_t kind = DynamicNull; + // Holds a number, or a bool as 0 or 1. + double number = 0.0; + std::string text; + std::vector bytes; + std::vector> children; +}; + +struct Scope { + std::vector nodes; +}; + +std::mutex& scopesMutex() { + static std::mutex mutex; + return mutex; +} + +std::unordered_map& scopes() { + static std::unordered_map map; + return map; +} + +std::atomic& nextScopeId() { + static std::atomic next{1}; + return next; +} + +Node& nodeAt(uint32_t scope, uint32_t node) { + std::lock_guard lock(scopesMutex()); + auto found = scopes().find(scope); + if (found == scopes().end()) { + throw std::runtime_error("dynamic scope is gone"); + } + if (node >= found->second.nodes.size()) { + throw std::runtime_error("dynamic node is gone"); + } + return found->second.nodes[node]; +} + +void checkKind(const Node& node, uint8_t kind, const char* what) { + if (node.kind != kind) { + throw std::runtime_error(std::string("expected ") + what); + } +} + +/// Adds an empty node and returns its id. Callers must not hold a reference to +/// another node across this, since the vector may move. +uint32_t appendNode(Scope& scope) { + scope.nodes.emplace_back(); + return static_cast(scope.nodes.size() - 1); +} + +} // namespace + +uint32_t dynamic_scope_open() { + const uint32_t id = nextScopeId().fetch_add(1); + std::lock_guard lock(scopesMutex()); + scopes().emplace(id, Scope{}); + return id; +} + +void dynamic_scope_close(uint32_t scope) { + std::lock_guard lock(scopesMutex()); + scopes().erase(scope); +} + +uint32_t dynamic_node_new(uint32_t scope, uint8_t kind) { + std::lock_guard lock(scopesMutex()); + auto found = scopes().find(scope); + if (found == scopes().end()) { + throw std::runtime_error("dynamic scope is gone"); + } + found->second.nodes.emplace_back(); + Node& node = found->second.nodes.back(); + node.kind = kind; + return static_cast(found->second.nodes.size() - 1); +} + +void dynamic_node_set_bool(uint32_t scope, uint32_t node, bool value) { + Node& target = nodeAt(scope, node); + checkKind(target, DynamicBool, "a boolean"); + target.number = value ? 1.0 : 0.0; +} + +void dynamic_node_set_number(uint32_t scope, uint32_t node, double value) { + Node& target = nodeAt(scope, node); + checkKind(target, DynamicNumber, "a number"); + target.number = value; +} + +void dynamic_node_set_buffer( + uint32_t scope, + uint32_t node, + rust::Slice bytes) { + Node& target = nodeAt(scope, node); + checkKind(target, DynamicBuffer, "an ArrayBuffer"); + target.bytes.assign(bytes.begin(), bytes.end()); +} + +void dynamic_node_set_text(uint32_t scope, uint32_t node, rust::Str text) { + Node& target = nodeAt(scope, node); + checkKind(target, DynamicString, "a string"); + target.text = std::string(text); +} + +void dynamic_node_append( + uint32_t scope, + uint32_t parent, + uint32_t child, + rust::Str key) { + Node& target = nodeAt(scope, parent); + if (target.kind != DynamicArray && target.kind != DynamicObject) { + throw std::runtime_error("expected an array or an object"); + } + target.children.emplace_back(std::string(key), child); +} + +uint8_t dynamic_node_kind(uint32_t scope, uint32_t node) { + return nodeAt(scope, node).kind; +} + +bool dynamic_node_bool(uint32_t scope, uint32_t node) { + const Node& target = nodeAt(scope, node); + checkKind(target, DynamicBool, "a boolean"); + return target.number != 0.0; +} + +double dynamic_node_number(uint32_t scope, uint32_t node) { + const Node& target = nodeAt(scope, node); + checkKind(target, DynamicNumber, "a number"); + return target.number; +} + +rust::String dynamic_node_text(uint32_t scope, uint32_t node) { + const Node& target = nodeAt(scope, node); + checkKind(target, DynamicString, "a string"); + return rust::String(target.text); +} + +size_t dynamic_node_buffer_len(uint32_t scope, uint32_t node) { + const Node& target = nodeAt(scope, node); + checkKind(target, DynamicBuffer, "an ArrayBuffer"); + return target.bytes.size(); +} + +rust::Vec dynamic_node_buffer(uint32_t scope, uint32_t node) { + const Node& target = nodeAt(scope, node); + checkKind(target, DynamicBuffer, "an ArrayBuffer"); + rust::Vec bytes; + bytes.reserve(target.bytes.size()); + for (const uint8_t byte : target.bytes) { + bytes.push_back(byte); + } + return bytes; +} + +size_t dynamic_node_len(uint32_t scope, uint32_t node) { + const Node& target = nodeAt(scope, node); + if (target.kind != DynamicArray && target.kind != DynamicObject) { + throw std::runtime_error("expected an array or an object"); + } + return target.children.size(); +} + +uint32_t dynamic_node_child(uint32_t scope, uint32_t node, size_t index) { + const Node& target = nodeAt(scope, node); + if (index >= target.children.size()) { + throw std::runtime_error("child index out of range"); + } + return target.children[index].second; +} + +rust::String dynamic_node_key(uint32_t scope, uint32_t node, size_t index) { + const Node& target = nodeAt(scope, node); + if (index >= target.children.size()) { + throw std::runtime_error("child index out of range"); + } + return rust::String(target.children[index].first); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.h new file mode 100644 index 0000000000..99a3b9e391 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiDynamic.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include + +#include + +namespace rngpui { + +/// A value crosses the cxx bridge as nodes in a C++-owned arena, one scope per +/// call, so nothing is serialised to text and ownership stays on one side. The +/// side that opens a scope closes it. +enum DynamicKind : uint8_t { + DynamicNull = 0, + DynamicBool = 1, + DynamicNumber = 2, + DynamicString = 3, + DynamicArray = 4, + DynamicObject = 5, + DynamicBuffer = 6, +}; + +constexpr uint32_t kDynamicNoNode = 0xffffffffu; + +uint32_t dynamic_scope_open(); +void dynamic_scope_close(uint32_t scope); + +/// `key` names an object member; an array child has none. +uint32_t dynamic_node_new(uint32_t scope, uint8_t kind); +void dynamic_node_set_bool(uint32_t scope, uint32_t node, bool value); +void dynamic_node_set_number(uint32_t scope, uint32_t node, double value); +void dynamic_node_set_text(uint32_t scope, uint32_t node, rust::Str text); +void dynamic_node_set_buffer( + uint32_t scope, + uint32_t node, + rust::Slice bytes); +void dynamic_node_append( + uint32_t scope, + uint32_t parent, + uint32_t child, + rust::Str key); + +uint8_t dynamic_node_kind(uint32_t scope, uint32_t node); +bool dynamic_node_bool(uint32_t scope, uint32_t node); +double dynamic_node_number(uint32_t scope, uint32_t node); +rust::String dynamic_node_text(uint32_t scope, uint32_t node); +size_t dynamic_node_len(uint32_t scope, uint32_t node); +size_t dynamic_node_buffer_len(uint32_t scope, uint32_t node); +rust::Vec dynamic_node_buffer(uint32_t scope, uint32_t node); +uint32_t dynamic_node_child(uint32_t scope, uint32_t node, size_t index); +rust::String dynamic_node_key(uint32_t scope, uint32_t node, size_t index); + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHost.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHost.cpp new file mode 100644 index 0000000000..f1c4ddf83e --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHost.cpp @@ -0,0 +1,1081 @@ +#include "GpuiHost.h" +#include + +#include +#include +#include "GpuiHttpClient.h" +#include "GpuiJsEngine.h" +#include "GpuiLogBoxSurfaceDelegate.h" +#include "GpuiModules.h" +#include "GpuiMountingManager.h" +#include "GpuiNodeAddons.h" +#include "GpuiRustTurboModules.h" +#include "MountBatchBuilder.h" +#include "rn-gpui-sys/src/lib.rs.h" // cxx bridge + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +std::atomic nativeAnimationActive{false}; +} // namespace + +extern "C" uint64_t rn_gpui_native_animations_active() noexcept { + return nativeAnimationActive.load() ? 1 : 0; +} + +namespace rngpui { +void updateFrameDemand() { + rn_gpui_set_animated_active( + nativeAnimationActive.load() || anyFrameRequested()); +} +} // namespace rngpui + +extern "C" uint64_t rn_gpui_frame_demand() noexcept { + return rngpui::anyFrameRequested() ? 1 : 0; +} + +extern "C" uint64_t rn_gpui_pending_work() noexcept { + return rngpui::pendingWork(); +} + +extern "C" bool rn_gpui_accessibility_active(); +#if defined(_MSC_VER) +// This hook lives in the rn-gpui Rust crate, which rn-gpui-sys does not depend +// on. An application links both, but rn-gpui-sys's own test binaries link only +// this crate, and MSVC has no weak symbols, so the reference would be +// unresolved (LNK2019). `alternatename` names a fallback used only while the +// symbol is otherwise undefined, so a real definition still wins wherever the +// crate is present. +extern "C" bool rn_gpui_accessibility_active_default() { + return false; +} +#pragma comment( \ + linker, \ + "/alternatename:rn_gpui_accessibility_active=rn_gpui_accessibility_active_default") +#endif + +#if defined(__APPLE__) +extern "C" bool rn_gpui_get_reduce_motion(); +extern "C" bool rn_gpui_get_reduce_transparency(); +extern "C" bool rn_gpui_get_increase_contrast(); +extern "C" bool rn_gpui_get_invert_colors(); +extern "C" bool rn_gpui_get_voice_over(); +extern "C" void rn_gpui_announce_accessibility( + const char* message, + int priorityLevel); +extern "C" void rn_gpui_start_accessibility_observing(); +extern "C" void rn_gpui_stop_accessibility_observing(); +#endif + +using namespace facebook::react; + +namespace rngpui { +namespace { +std::mutex pointerEventMutex; +extension::RawPointerEvent lastPointerEvent{}; +} // namespace + +namespace { +std::atomic activeGpuiHost{nullptr}; + +namespace jsi = facebook::jsi; +using AppCppTurboModuleProvider = std::shared_ptr (*)( + const std::string&, + std::shared_ptr); +std::atomic appCppTurboModuleProvider{nullptr}; + +std::mutex appearanceMutex; +std::string systemAppearance{"light"}; +std::optional appearanceOverride; + +class GpuiAppearanceModule final + : public NativeAppearanceCxxSpec { + public: + explicit GpuiAppearanceModule(std::shared_ptr jsInvoker) + : NativeAppearanceCxxSpec(std::move(jsInvoker)) {} + + facebook::jsi::String getColorScheme(facebook::jsi::Runtime& runtime) { + std::lock_guard lock(appearanceMutex); + return facebook::jsi::String::createFromUtf8( + runtime, appearanceOverride.value_or(systemAppearance)); + } + + void setColorScheme(facebook::jsi::Runtime&, std::string scheme) { + std::lock_guard lock(appearanceMutex); + if (scheme == "light" || scheme == "dark") { + appearanceOverride = std::move(scheme); + } else { + appearanceOverride.reset(); + } + } + + void addListener(facebook::jsi::Runtime&, std::string) {} + void removeListeners(facebook::jsi::Runtime&, double) {} +}; + +class GpuiAccessibilityManagerModule final + : public NativeAccessibilityManagerCxxSpec { + public: + explicit GpuiAccessibilityManagerModule( + std::shared_ptr jsInvoker) + : NativeAccessibilityManagerCxxSpec(std::move(jsInvoker)) {} + + void getCurrentBoldTextState( + jsi::Runtime& /*runtime*/, + AsyncCallback onSuccess, + AsyncCallback /*onError*/) { + onSuccess(false); + } + void getCurrentGrayscaleState( + jsi::Runtime& /*runtime*/, + AsyncCallback onSuccess, + AsyncCallback /*onError*/) { + onSuccess(false); + } + void getCurrentInvertColorsState( + jsi::Runtime& /*runtime*/, + AsyncCallback onSuccess, + AsyncCallback /*onError*/) { +#if defined(__APPLE__) + onSuccess(rn_gpui_get_invert_colors()); +#else + onSuccess(false); +#endif + } + void getCurrentReduceMotionState( + jsi::Runtime& /*runtime*/, + AsyncCallback onSuccess, + AsyncCallback /*onError*/) { +#if defined(__APPLE__) + onSuccess(rn_gpui_get_reduce_motion()); +#else + onSuccess(false); +#endif + } + void getCurrentDarkerSystemColorsState( + jsi::Runtime& /*runtime*/, + AsyncCallback onSuccess, + AsyncCallback /*onError*/) { +#if defined(__APPLE__) + onSuccess(rn_gpui_get_increase_contrast()); +#else + onSuccess(false); +#endif + } + void getCurrentPrefersCrossFadeTransitionsState( + jsi::Runtime& /*runtime*/, + AsyncCallback onSuccess, + AsyncCallback /*onError*/) { + onSuccess(false); + } + void getCurrentReduceTransparencyState( + jsi::Runtime& /*runtime*/, + AsyncCallback onSuccess, + AsyncCallback /*onError*/) { +#if defined(__APPLE__) + onSuccess(rn_gpui_get_reduce_transparency()); +#else + onSuccess(false); +#endif + } + void getCurrentVoiceOverState( + jsi::Runtime& /*runtime*/, + AsyncCallback onSuccess, + AsyncCallback /*onError*/) { +#if defined(__APPLE__) + onSuccess(rn_gpui_get_voice_over()); +#else + onSuccess(rn_gpui_accessibility_active()); +#endif + } + void setAccessibilityContentSizeMultipliers( + jsi::Runtime& /*runtime*/, + jsi::Object /*multipliers*/) {} + void setAccessibilityFocus(jsi::Runtime& /*runtime*/, double reactTag) { + rn_gpui_request_accessibility_focus(static_cast(reactTag)); + } + void announceForAccessibility( + jsi::Runtime& runtime, + jsi::String announcement) { +#if defined(__APPLE__) + auto text = announcement.utf8(runtime); + rn_gpui_announce_accessibility(text.c_str(), 1); +#endif + } + void announceForAccessibilityWithOptions( + jsi::Runtime& runtime, + jsi::String announcement, + jsi::Object options) { +#if defined(__APPLE__) + auto text = announcement.utf8(runtime); + const auto queue = options.getProperty(runtime, "queue"); + int priorityLevel = queue.isBool() && queue.getBool() ? 0 : 1; + const auto priority = options.getProperty(runtime, "priority"); + if (priority.isString()) { + const auto name = priority.asString(runtime).utf8(runtime); + if (name == "high") + priorityLevel = 2; + else if (name == "low") + priorityLevel = 0; + else if (name == "default") + priorityLevel = 1; + } + rn_gpui_announce_accessibility(text.c_str(), priorityLevel); +#endif + } +}; + +class GpuiAccessibilityInfoModule final + : public NativeAccessibilityInfoCxxSpec { + public: + explicit GpuiAccessibilityInfoModule(std::shared_ptr jsInvoker) + : NativeAccessibilityInfoCxxSpec(std::move(jsInvoker)) {} + + void isReduceMotionEnabled(jsi::Runtime&, AsyncCallback callback) { +#if defined(__APPLE__) + callback(rn_gpui_get_reduce_motion()); +#else + callback(false); +#endif + } + void isInvertColorsEnabled(jsi::Runtime&, AsyncCallback callback) { +#if defined(__APPLE__) + callback(rn_gpui_get_invert_colors()); +#else + callback(false); +#endif + } + void isHighTextContrastEnabled(jsi::Runtime&, AsyncCallback callback) { +#if defined(__APPLE__) + callback(rn_gpui_get_increase_contrast()); +#else + callback(false); +#endif + } + void isTouchExplorationEnabled(jsi::Runtime&, AsyncCallback callback) { + // Desktop pointer/keyboard accessibility is not equivalent to touch + // exploration; AccessKit currently has no portable screen-reader query. + callback(false); + } + void isAccessibilityServiceEnabled( + jsi::Runtime&, + AsyncCallback callback) { + callback(false); + } + void setAccessibilityFocus(jsi::Runtime&, double reactTag) { + rn_gpui_request_accessibility_focus(static_cast(reactTag)); + } + void announceForAccessibility(jsi::Runtime& runtime, jsi::String text) { +#if defined(__APPLE__) + const auto announcement = text.utf8(runtime); + rn_gpui_announce_accessibility(announcement.c_str(), 1); +#endif + } + void getRecommendedTimeoutMillis( + jsi::Runtime&, + double timeout, + AsyncCallback callback) { + callback(timeout); + } + void isGrayscaleEnabled(jsi::Runtime&, AsyncCallback callback) { + callback(false); + } +}; + +extern "C" void rn_gpui_request_reload(); + +/// Thin C shim so the vendored `DevSettingsModule` (ReactCxxPlatform) can reach +/// the same Rust handler without depending on the cxx bridge header. +extern "C" void rn_gpui_dev_settings_changed(const char* name, bool enabled) { + if (name != nullptr) { + rn_gpui_dev_setting(rust::String(name), enabled); + } +} + +extern "C" void rn_gpui_set_app_cpp_turbo_module_provider( + AppCppTurboModuleProvider provider) { + appCppTurboModuleProvider.store(provider, std::memory_order_release); +} + +TurboModuleProviders gpui_turbo_module_providers() { + return { + [](const std::string& name, const std::shared_ptr& jsInvoker) + -> std::shared_ptr { + if (auto provider = + appCppTurboModuleProvider.load(std::memory_order_acquire)) { + if (auto appModule = provider(name, jsInvoker)) { + return appModule; + } + } + for (const auto& extensionProvider : + rngpui::gpuiModuleHooks().turboModules) { + if (auto extensionModule = extensionProvider(name, jsInvoker)) { + return extensionModule; + } + } + if (name == + NativeAppearanceCxxSpec::kModuleName) { + return std::make_shared(jsInvoker); + } + if (name == + NativeAccessibilityManagerCxxSpec< + GpuiAccessibilityManagerModule>::kModuleName) { + return std::make_shared(jsInvoker); + } + if (name == + NativeAccessibilityInfoCxxSpec< + GpuiAccessibilityInfoModule>::kModuleName) { + return std::make_shared(jsInvoker); + } + return nullptr; + }}; +} + +// Installs the OSS stable feature flags, which is what an OSS app on the new +// (bridgeless) architecture does. `enableBridgelessArchitecture` is load +// bearing for us: ReactHost here IS the bridgeless host, and an engine is +// expected to enable its microtask queue when that flag is on; without it, +// React's rendering path fails with "Could not enqueue microtask because they +// are disabled in this runtime". +void install_feature_flags() { + static std::once_flag once; + std::call_once(once, [] { + // An engine that needs flags of its own returns an overrides object + // deriving from rngpui::GpuiFeatureFlagsOverrides, so core's survive. + const auto& engine = rngpui::activeJsEngine(); + std::unique_ptr overrides; + if (engine.makeFeatureFlagsOverrides) { + overrides = engine.makeFeatureFlagsOverrides(); + } + if (!overrides) { + overrides = std::make_unique(); + } + ReactNativeFeatureFlags::override(std::move(overrides)); + }); +} +} // namespace + +std::unique_ptr new_gpui_host( + rust::Str appId, + rust::Str devHost, + uint32_t devPort, + rust::Str jsEngine, + bool devMode) { + return std::make_unique(appId, devHost, devPort, jsEngine, devMode); +} + +GpuiHost::GpuiHost( + rust::Str appId, + rust::Str devHost, + uint32_t devPort, + rust::Str jsEngine, + bool devMode) { + // Engine packages register themselves here (the generated + // rn_gpui_install_modules()), so the registry is populated before the + // app's choice (`RN_GPUI_JS_ENGINE` / DesktopAppConfig::js_engine) is applied + // and before anything asks for the active engine. The Rust TurboModule + // provider is always present, so it installs its hook alongside them. + rngpui::install_rust_turbo_modules(); + rngpui::rn_gpui_install_modules(); + rngpui::selectJsEngine(std::string(jsEngine)); + install_feature_flags(); + rngpui::installGpuiTextMeasureTable(); + + ReactInstanceConfig config; + config.appId = std::string(appId); + config.deviceName = "rn-gpui"; + config.devServerHost = std::string(devHost); + config.devServerPort = devPort; + // Dev mode is decided at runtime by the caller (the app's Cargo profile, or + // RN_GPUI_DEV_MODE), not by REACT_NATIVE_DEBUG. The prebuilt runtime is built + // in Release, so a macro default here would send every debug app that links + // it to the on-disk bundle instead of Metro. + config.enableDevMode = devMode; + config.enableInspector = devMode; + if (std::getenv("RN_GPUI_TEST_DRIVER")) { + const auto* mode = std::getenv("RN_GPUI_TEST_MODE"); + if (mode && std::string_view(mode) == "metro") + config.enableDevMode = true; + } + config.platformTimerRegistryFactory = [] { + return std::make_unique(); + }; + config.jsRuntimeFactory = rngpui::activeJsEngine().makeRuntimeFactory(); + + mountingManager_ = std::make_shared( + [this](SurfaceId surfaceId) { + reactHost_->runOnScheduler([surfaceId](Scheduler& scheduler) { + scheduler.reportMount(surfaceId); + }); + }, + [this](SurfaceId surfaceId) { + return reactHost_->surfaceContentSize(surfaceId); + }); + + rngpui::setHostComponentStateManager(mountingManager_); + + // ReactHost's constructor throws unless both factories are in the + // ContextContainer. + auto contextContainer = std::make_shared(); + contextContainer->insert(HttpClientFactoryKey, gpui_http_client_factory()); + contextContainer->insert( + WebSocketClientFactoryKey, getWebSocketClientFactory()); + contextContainer->insert( + "RCTImageLoader", + std::static_pointer_cast(mountingManager_->getImageLoader())); + + runLoopObserverManager_ = std::make_shared(); + + config.animatedNodesManagerProviderFactory = [this] { + return std::make_shared( + [this](std::function&& callback, bool /*isAsync*/) { + if (gpuiDebugAnimationEnabled()) { + LOG(INFO) << "[native-animated] frame callback started"; + } + { + std::lock_guard lock(animatedFrameMutex_); + animatedFrameCallback_ = std::move(callback); + } + nativeAnimationActive.store(true); + rngpui::updateFrameDemand(); + }, + [this](bool /*isAsync*/) { + if (gpuiDebugAnimationEnabled()) { + LOG(INFO) << "[native-animated] frame callback stopped"; + } + { + std::lock_guard lock(animatedFrameMutex_); + animatedFrameCallback_ = nullptr; + } + nativeAnimationActive.store(false); + rngpui::updateFrameDemand(); + }); + }; + config.beforeInstanceDestroy = [this] { + // Wait for a frame already executing on GPUI, then prevent callbacks into + // the old graph while ReactHost tears down its runtime and UIManager. + std::lock_guard lock(animatedFrameMutex_); + animatedFrameCallback_ = nullptr; + nativeAnimationActive.store(false); + rngpui::updateFrameDemand(); + }; + + // The LogBox surface. ReactHost hands this to + // `ReactCxxTurboModuleProvider`, which builds `LogBoxModule` only when the + // delegate is set (`ReactCxxTurboModuleProvider.cpp`); without it the JS side + // logs "Failed to load TurboModule: LogBox" and no red box appears. + logBoxDelegate_ = std::make_shared(*this); + + reactHost_ = std::make_unique( + config, + mountingManager_, + runLoopObserverManager_, + std::move(contextContainer), + /* onJsError */ + [](facebook::jsi::Runtime&, const JsErrorHandler::ProcessedError& error) { + rn_gpui_log(3, error.message); + for (const auto& frame : error.stack) { + std::string entry = " at " + frame.methodName; + if (frame.file) + entry += " (" + *frame.file; + if (frame.lineNumber) + entry += ":" + std::to_string(*frame.lineNumber); + if (frame.column) + entry += ":" + std::to_string(*frame.column); + if (frame.file) + entry += ")"; + rn_gpui_log(3, entry); + } + }, + /* logger */ + [](const std::string& message, unsigned int level) { + rn_gpui_log(static_cast(level), message); + }, + /* devUIDelegate */ nullptr, + /* turboModuleProviders */ gpui_turbo_module_providers(), + /* logBoxSurfaceDelegate */ logBoxDelegate_, + /* animatedNodesManagerProvider */ animatedNodesManagerProvider_, + /* bindingsInstallFunc */ + [](facebook::jsi::Runtime& runtime) { + // `_requireNodeLibrary` must exist before the app bundle runs, and a + // reload creates a new runtime, so it is installed here rather than at + // module-registration time. ReactInstance installs the + // RuntimeSchedulerBinding just before calling this, which is the same + // CallInvoker the TurboModules get; a fresh one per runtime is correct, + // because the engine's host callbacks belong to that runtime. + auto binding = + facebook::react::RuntimeSchedulerBinding::getBinding(runtime); + if (!binding) { + return; + } + rngpui::installNodeAddons( + runtime, + std::make_shared( + binding->getRuntimeScheduler())); + }); + activeGpuiHost.store(this, std::memory_order_release); +#if defined(__APPLE__) + rn_gpui_start_accessibility_observing(); +#endif +} + +GpuiHost::~GpuiHost() { +#if defined(__APPLE__) + rn_gpui_stop_accessibility_observing(); +#endif + reactHost_->stopAllSurfaces(); + for (const auto& listener : eventListeners_) { + reactHost_->runOnScheduler([&listener](Scheduler& scheduler) { + scheduler.removeEventListener(listener); + }); + } + auto* expected = this; + activeGpuiHost.compare_exchange_strong( + expected, nullptr, std::memory_order_acq_rel); +} + +std::shared_ptr GpuiHost::ui_manager() const { + std::shared_ptr result; + reactHost_->runOnScheduler( + [&result](Scheduler& scheduler) { result = scheduler.getUIManager(); }); + return result; +} + +void GpuiHost::synchronously_update_props( + int32_t tag, + const folly::dynamic& props) const { + mountingManager_->synchronouslyUpdateViewOnUIThread(tag, props); +} + +void GpuiHost::add_event_listener( + std::shared_ptr listener) { + reactHost_->runOnScheduler([&listener](Scheduler& scheduler) { + scheduler.addEventListener(listener); + }); + eventListeners_.push_back(std::move(listener)); +} + +std::shared_ptr get_gpui_ui_manager() { + auto* host = activeGpuiHost.load(std::memory_order_acquire); + return host ? host->ui_manager() : nullptr; +} + +void gpui_synchronously_update_props(int32_t tag, const folly::dynamic& props) { + if (auto* host = activeGpuiHost.load(std::memory_order_acquire)) { + host->synchronously_update_props(tag, props); + } +} + +void register_gpui_event_listener( + std::shared_ptr listener) { + if (auto* host = activeGpuiHost.load(std::memory_order_acquire)) { + host->add_event_listener(std::move(listener)); + } +} + +bool GpuiHost::load_script(rust::Str bundlePath, rust::Str sourceUrl) { + scriptLoaded_ = + reactHost_->loadScript(std::string(bundlePath), std::string(sourceUrl)); + return scriptLoaded_; +} + +namespace { +/// Fabric asserts on an undefined constraint, so every bound is a real number +/// and the pair is ordered: a caller that asks for a range wider than its +/// content still gets a valid constraint. +facebook::react::LayoutConstraints constraintsFrom( + float minWidth, + float minHeight, + float maxWidth, + float maxHeight, + float /*scale*/) { + facebook::react::LayoutConstraints constraints; + const float minW = std::max(0.f, minWidth); + const float minH = std::max(0.f, minHeight); + const float maxW = std::max(minW, maxWidth); + const float maxH = std::max(minH, maxHeight); + constraints.minimumSize = {minW, minH}; + constraints.maximumSize = {maxW, maxH}; + constraints.layoutDirection = facebook::react::LayoutDirection::LeftToRight; + return constraints; +} +} // namespace + +void GpuiHost::start_surface( + int32_t id, + rust::Str module, + float w, + float h, + float scale) { + start_surface_with_props(id, module, rust::Str("{}"), w, h, scale); +} + +void GpuiHost::start_surface_with_props( + int32_t id, + rust::Str module, + rust::Str propsJson, + float w, + float h, + float scale) { + LayoutConstraints constraints; + constraints.minimumSize = {w, h}; + constraints.maximumSize = {w, h}; + // SurfaceHandler asserts on Undefined; GPUI surfaces are LTR for now (an RTL + // flag would come from the GPUI window's text direction). + constraints.layoutDirection = LayoutDirection::LeftToRight; + LayoutContext context; + context.pointScaleFactor = scale; + if (id == kAppSurfaceId) { + surfaceWidth_ = w; + surfaceHeight_ = h; + surfaceScale_ = scale; + mountingManager_->setWindowSize(w, h); + } + folly::dynamic initialProps = folly::dynamic::object(); + const auto props = std::string(propsJson); + if (!props.empty()) { + auto parsed = folly::parseJson(props); + if (!parsed.isObject()) + throw std::invalid_argument("Surface initialProps must be an object"); + initialProps = std::move(parsed); + } + if (id == kAppSurfaceId && std::getenv("RN_GPUI_TEST_DRIVER")) { + if (const char* value = std::getenv("RN_GPUI_TEST_INITIAL_PROPS")) { + initialProps = folly::parseJson(value); + if (!initialProps.isObject()) + throw std::invalid_argument("Test initialProps must be an object"); + } + } + reactHost_->startSurface( + id, std::string(module), initialProps, constraints, context); +} + +void GpuiHost::start_surface_with_constraints( + int32_t id, + rust::Str module, + rust::Str propsJson, + float minWidth, + float minHeight, + float maxWidth, + float maxHeight, + float scale) { + LayoutConstraints constraints = + constraintsFrom(minWidth, minHeight, maxWidth, maxHeight, scale); + LayoutContext context; + context.pointScaleFactor = scale; + if (id == kAppSurfaceId) { + surfaceWidth_ = maxWidth; + surfaceHeight_ = maxHeight; + surfaceScale_ = scale; + mountingManager_->setWindowSize(maxWidth, maxHeight); + } + folly::dynamic initialProps = folly::dynamic::object(); + const auto props = std::string(propsJson); + if (!props.empty()) { + auto parsed = folly::parseJson(props); + if (!parsed.isObject()) + throw std::invalid_argument("Surface initialProps must be an object"); + initialProps = std::move(parsed); + } + reactHost_->startSurface( + id, std::string(module), initialProps, constraints, context); +} + +void GpuiHost::set_surface_constraints( + int32_t id, + float minWidth, + float minHeight, + float maxWidth, + float maxHeight, + float scale) { + LayoutConstraints constraints = + constraintsFrom(minWidth, minHeight, maxWidth, maxHeight, scale); + LayoutContext context; + context.pointScaleFactor = scale; + if (id == kAppSurfaceId) { + surfaceWidth_ = maxWidth; + surfaceHeight_ = maxHeight; + surfaceScale_ = scale; + mountingManager_->setWindowSize(maxWidth, maxHeight); + } + reactHost_->setSurfaceConstraints(id, constraints, context); +} + +void GpuiHost::set_surface_size(int32_t id, float w, float h, float scale) { + LayoutConstraints constraints; + constraints.minimumSize = {w, h}; + constraints.maximumSize = {w, h}; + constraints.layoutDirection = LayoutDirection::LeftToRight; + LayoutContext context; + context.pointScaleFactor = scale; + if (id == kAppSurfaceId) { + surfaceWidth_ = w; + surfaceHeight_ = h; + surfaceScale_ = scale; + mountingManager_->setWindowSize(w, h); + } + reactHost_->setSurfaceConstraints(id, constraints, context); + set_window_metrics(w, h, scale); +} + +void GpuiHost::set_window_top_inset(float top) { + mountingManager_->setWindowTopInset(top); +} + +void GpuiHost::set_window_metrics(float w, float h, float scale) { + if (!DeviceInfoModule::setWindowMetrics(w, h, scale) || !scriptLoaded_) { + return; + } + const folly::dynamic metrics = folly::dynamic::object("width", w)( + "height", h)("scale", scale)("fontScale", 1.0); + folly::dynamic payload = + folly::dynamic::object("window", metrics)("screen", metrics); + reactHost_->emitDeviceEvent( + folly::dynamic::array("didUpdateDimensions", std::move(payload))); +} + +void GpuiHost::emit_device_event(rust::Str eventName) const { + if (!scriptLoaded_) + return; + const auto timestamp = + std::chrono::duration( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + reactHost_->emitDeviceEvent(folly::dynamic::array( + std::string(eventName), folly::dynamic::object("timeStamp", timestamp))); +} + +void GpuiHost::emit_device_event_bool(rust::Str eventName, bool value) const { + if (scriptLoaded_) { + reactHost_->emitDeviceEvent( + folly::dynamic::array(std::string(eventName), value)); + } +} + +void GpuiHost::emit_device_event_json( + rust::Str eventName, + rust::Str payloadJson) const { + if (!scriptLoaded_) + return; + folly::dynamic payload = folly::dynamic::object(); + try { + auto parsed = folly::parseJson(std::string(payloadJson)); + if (parsed.isObject()) { + payload = std::move(parsed); + } + } catch (const std::exception& error) { + LOG(WARNING) << "emit_device_event_json(" << std::string(eventName) + << "): invalid payload: " << error.what(); + } + reactHost_->emitDeviceEvent( + folly::dynamic::array(std::string(eventName), std::move(payload))); +} + +void GpuiHost::emit_accessibility_setting_event( + const std::string& eventName, + bool enabled) const { + if (scriptLoaded_) { + reactHost_->emitDeviceEvent(folly::dynamic::array(eventName, enabled)); + } +} + +#if defined(__APPLE__) +extern "C" void rn_gpui_accessibility_setting_changed( + const char* eventName, + bool enabled) { + auto* host = activeGpuiHost.load(std::memory_order_acquire); + if (host != nullptr && eventName != nullptr) { + host->emit_accessibility_setting_event(eventName, enabled); + } +} +#endif + +void GpuiHost::set_system_appearance(rust::Str appearance) { + bool shouldNotify = false; + std::string nextAppearance; + { + std::lock_guard lock(appearanceMutex); + nextAppearance = std::string(appearance) == "dark" ? "dark" : "light"; + if (systemAppearance == nextAppearance) + return; + systemAppearance = nextAppearance; + shouldNotify = !appearanceOverride.has_value() && scriptLoaded_; + } + if (shouldNotify) { + folly::dynamic preferences = + folly::dynamic::object("colorScheme", nextAppearance); + reactHost_->emitDeviceEvent( + folly::dynamic::array("appearanceChanged", std::move(preferences))); + } +} + +void GpuiHost::open_debugger() const { + reactHost_->openDebugger(); +} + +namespace { +// Static, so a probe that outlives a reload or the host touches nothing that +// has been destroyed. +std::atomic jsHeapBytes{0}; + +/// The engine's own figure for "bytes allocated on the JS heap". JSI leaves +/// the keys of `getHeapInfo` to each engine, which prefix them with their +/// name, so this goes by the suffix instead of naming an engine. +uint64_t sampleJsHeapBytes(facebook::jsi::Runtime& runtime) { + uint64_t allocated = 0; + uint64_t size = 0; + try { + for (const auto& [key, value] : + runtime.instrumentation().getHeapInfo(false)) { + if (value <= 0) { + continue; + } + if (key.ends_with("allocatedBytes")) { + allocated = static_cast(value); + } else if (key.ends_with("heapSize")) { + size = static_cast(value); + } + } + } catch (...) { + } + return allocated != 0 ? allocated : size; +} +} // namespace + +uint64_t GpuiHost::js_frame_probe() const { + static std::atomic completed{0}; + static std::atomic pending{false}; + if (scriptLoaded_ && !reactHost_->isReloading() && !pending.exchange(true)) { + reactHost_->runOnRuntimeScheduler([](facebook::jsi::Runtime& runtime) { + static std::chrono::steady_clock::time_point sampledAt{}; + const auto now = std::chrono::steady_clock::now(); + if (now - sampledAt >= std::chrono::milliseconds(500)) { + sampledAt = now; + jsHeapBytes.store( + sampleJsHeapBytes(runtime), std::memory_order_relaxed); + } + completed.fetch_add(1, std::memory_order_relaxed); + pending.store(false); + }); + } + return completed.load(std::memory_order_relaxed); +} + +uint64_t GpuiHost::js_heap_bytes() const { + return jsHeapBytes.load(std::memory_order_relaxed); +} + +void GpuiHost::stop_surface(int32_t id) { + reactHost_->stopSurface(id); + // Fabric emits no Delete mutations for a surface that is torn down, so the + // store has to drop it explicitly. + rn_gpui_on_surface_stopped(id); +} + +void GpuiHost::request_reload() { + reactHost_->requestReload(); +} +bool GpuiHost::set_dev_server(rust::Str host, uint32_t port) { + return reactHost_->setDevServer(std::string(host), port); +} +rust::String GpuiHost::dev_server_address() const { + return rust::String(reactHost_->devServerAddress()); +} +bool GpuiHost::reload_in_progress() const { + return reactHost_->isReloading(); +} +uint64_t GpuiHost::instance_generation() const { + return reactHost_->instanceGeneration(); +} +extern "C" void rn_gpui_request_reload() { + if (auto* host = activeGpuiHost.load()) + host->request_reload(); +} +extern "C" bool rn_gpui_reload_in_progress() { + if (auto* host = activeGpuiHost.load()) + return host->reload_in_progress(); + return false; +} +extern "C" uint64_t rn_gpui_instance_generation() { + if (auto* host = activeGpuiHost.load()) + return host->instance_generation(); + return 0; +} + +void GpuiHost::start_log_box_surface() { + LayoutConstraints constraints; + constraints.minimumSize = {surfaceWidth_, surfaceHeight_}; + constraints.maximumSize = {surfaceWidth_, surfaceHeight_}; + constraints.layoutDirection = LayoutDirection::LeftToRight; + LayoutContext context; + context.pointScaleFactor = surfaceScale_; + reactHost_->startSurface( + kLogBoxSurfaceId, + "LogBox", + folly::dynamic::object(), + constraints, + context); +} + +/// Stop the LogBox surface again: `hide()` is what keeps a closed +/// LogBox from covering the app surface with an empty full-size overlay. +void GpuiHost::stop_log_box_surface() { + reactHost_->stopSurface(kLogBoxSurfaceId); + rn_gpui_on_surface_stopped(kLogBoxSurfaceId); +} + +void GpuiHost::on_render() const { + std::lock_guard lock(animatedFrameMutex_); + if (reactHost_->isReloading()) + return; + runLoopObserverManager_->onRender(); +} + +namespace { +void dispatchPointerTick( + GpuiMountingManager& mountingManager, + double eventTimestampMs) { + if (gpuiExtensionHooks().pointerObservers.empty()) + return; + extension::RawPointerEvent tick; + { + std::lock_guard lock(pointerEventMutex); + tick = lastPointerEvent; + } + tick.phase = 4; // native time tick for duration-based recognizers + tick.timestamp = eventTimestampMs; + for (const auto& observer : gpuiExtensionHooks().pointerObservers) { + for (const auto& recognized : observer(tick)) { + mountingManager.dispatchEvent( + recognized.targetTag, + recognized.eventType, + folly::parseJson(recognized.payloadJson)); + } + } +} +} // namespace + +void GpuiHost::on_pointer_frame(double eventTimestampMs) const { + std::lock_guard lock(animatedFrameMutex_); + if (reactHost_->isReloading()) + return; + dispatchPointerTick(*mountingManager_, eventTimestampMs); +} + +void GpuiHost::on_frame(double eventTimestampMs) const { + std::lock_guard lock(animatedFrameMutex_); + if (reactHost_->isReloading()) + return; + dispatchPointerTick(*mountingManager_, eventTimestampMs); + on_render(); + if (const auto& frame = rngpui::gpuiModuleHooks().frame) { + const auto frameTimestampMs = + std::chrono::duration( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + frame(frameTimestampMs); + } + { + const auto frameTimestampMs = + std::chrono::duration( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + for (const auto& frame : rngpui::gpuiExtensionHooks().frameCallbacks) { + frame(frameTimestampMs); + } + } + std::function animationFrame; + { + std::lock_guard lock(animatedFrameMutex_); + animationFrame = animatedFrameCallback_; + } + if (animationFrame) { + if (gpuiDebugAnimationEnabled()) { + static std::atomic frames{0}; + const auto frame = ++frames; + if (frame % 60 == 0) { + LOG(INFO) << "[native-animated] GPUI delivered frame " << frame; + } + } + animationFrame(); + } +} + +void GpuiHost::dispatch_event( + int32_t tag, + rust::Str type, + rust::Str payloadJson) const { + mountingManager_->dispatchEvent( + tag, std::string(type), folly::parseJson(std::string(payloadJson))); +} + +void GpuiHost::dispatch_pointer_event( + rust::Str targetChainJson, + float x, + float y, + int32_t phase, + double timestamp) const { + extension::RawPointerEvent event; + try { + const auto targetChain = folly::parseJson(std::string(targetChainJson)); + if (!targetChain.isArray()) + return; + event.targetChain.reserve(targetChain.size()); + for (const auto& tag : targetChain) { + if (tag.isInt()) + event.targetChain.push_back(static_cast(tag.asInt())); + } + } catch (const std::exception& error) { + LOG(WARNING) << "[pointer] invalid target chain: " << error.what(); + return; + } + event.x = x; + event.y = y; + event.phase = phase; + event.timestamp = timestamp; + { + std::lock_guard lock(pointerEventMutex); + lastPointerEvent = event; + } + + for (const auto& observer : gpuiExtensionHooks().pointerObservers) { + for (const auto& recognized : observer(event)) { + mountingManager_->dispatchEvent( + recognized.targetTag, + recognized.eventType, + folly::parseJson(recognized.payloadJson)); + } + } +} + +void GpuiHost::set_scroll_offset(int32_t tag, float x, float y) const { + mountingManager_->setScrollOffset(tag, x, y); +} +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHost.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHost.h new file mode 100644 index 0000000000..27acea54ab --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHost.h @@ -0,0 +1,164 @@ +// cxx compares the exact signature of each member function, so `&str` +// parameters must be `rust::Str`; a `const std::string&` fails with +// "type mismatch at 1st parameter". +#pragma once + +#include "rust/cxx.h" + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { +class ReactHost; +class RunLoopObserverManager; +class NativeAnimatedNodesManagerProvider; +class UIManager; +struct RawEvent; +using EventListener = std::function; +} // namespace facebook::react + +namespace folly { +// folly defines `dynamic` as a struct. MSVC puts the class/struct key into the +// mangled name (`V` vs `U`), so forward-declaring it as a class here mangles +// `gpui_synchronously_update_props` differently from the definition in +// GpuiHost.cpp, and the app-side archive fails to link with LNK2019. +struct dynamic; +} // namespace folly + +namespace rngpui { +class GpuiMountingManager; +class GpuiLogBoxSurfaceDelegate; + +constexpr int32_t kAppSurfaceId = 1; +constexpr int32_t kLogBoxSurfaceId = 2; +constexpr int32_t kAllSurfacesStopped = -1; + +class GpuiHost { + public: + GpuiHost( + rust::Str appId, + rust::Str devHost, + uint32_t devPort, + rust::Str jsEngine, + bool devMode); + ~GpuiHost(); + + bool load_script(rust::Str bundlePath, rust::Str sourceUrl); + void start_surface( + int32_t surfaceId, + rust::Str moduleName, + float w, + float h, + float scale); + void start_surface_with_props( + int32_t surfaceId, + rust::Str moduleName, + rust::Str propsJson, + float w, + float h, + float scale); + void set_surface_size(int32_t surfaceId, float w, float h, float scale); + /// Starts a surface whose size is a range rather than a point: `FitContent`. + /// Fabric lays the root out against these constraints and the + /// resulting size comes back in every mount batch as `content_width` / + /// `content_height`. + void start_surface_with_constraints( + int32_t surfaceId, + rust::Str moduleName, + rust::Str propsJson, + float minWidth, + float minHeight, + float maxWidth, + float maxHeight, + float scale); + /// Changes an existing surface's constraints, which is what a resize of an + /// embedded surface does. + void set_surface_constraints( + int32_t surfaceId, + float minWidth, + float minHeight, + float maxWidth, + float maxHeight, + float scale); + void set_window_metrics(float w, float h, float scale); + void set_window_top_inset(float top); + void set_system_appearance(rust::Str appearance); + void stop_surface(int32_t surfaceId); + void open_debugger() const; + uint64_t js_frame_probe() const; + uint64_t js_heap_bytes() const; + void on_render() const; + void on_frame(double eventTimestampMs) const; + void on_pointer_frame(double eventTimestampMs) const; + void dispatch_event(int32_t tag, rust::Str type, rust::Str payloadJson) const; + void emit_device_event(rust::Str event_name) const; + void emit_device_event_bool(rust::Str event_name, bool value) const; + void emit_device_event_json(rust::Str event_name, rust::Str payload_json) + const; + void emit_accessibility_setting_event( + const std::string& event_name, + bool enabled) const; + void dispatch_pointer_event( + rust::Str targetChainJson, + float x, + float y, + int32_t phase, + double timestamp) const; + void set_scroll_offset(int32_t tag, float x, float y) const; + std::shared_ptr ui_manager() const; + void synchronously_update_props(int32_t tag, const folly::dynamic& props) + const; + void add_event_listener( + std::shared_ptr listener); + + void request_reload(); + bool set_dev_server(rust::Str host, uint32_t port); + rust::String dev_server_address() const; + bool reload_in_progress() const; + /// Counts the React instances created so far; see + /// `ReactHost::instanceGeneration`. + uint64_t instance_generation() const; + void start_log_box_surface(); + void stop_log_box_surface(); + + private: + std::shared_ptr mountingManager_; + std::shared_ptr + runLoopObserverManager_; + /// Passed to ReactHost as its `logBoxSurfaceDelegate`. Created + /// before the host because ReactHost takes it in its constructor. + std::shared_ptr logBoxDelegate_; + /// Passed to ReactHost as its `animatedNodesManagerProvider`. It + /// is what makes `AnimatedModule` load; without it LogBox's inspector throws + /// "Native animated module is not available" and no red box renders. + std::shared_ptr + animatedNodesManagerProvider_; + mutable std::recursive_mutex animatedFrameMutex_; + mutable std::function animatedFrameCallback_; + std::unique_ptr reactHost_; + std::vector> + eventListeners_; + float surfaceWidth_{0.}; + float surfaceHeight_{0.}; + float surfaceScale_{1.}; + bool scriptLoaded_{false}; +}; + +std::unique_ptr new_gpui_host( + rust::Str appId, + rust::Str devHost, + uint32_t devPort, + rust::Str jsEngine, + bool devMode); +std::shared_ptr get_gpui_ui_manager(); +void gpui_synchronously_update_props(int32_t tag, const folly::dynamic& props); +void register_gpui_event_listener( + std::shared_ptr listener); +/// Runs the task a module posted with `rngpui::dispatchMain`. Called on the +/// GPUI main thread. +void rn_gpui_run_main_task(uint64_t id); +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponent.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponent.cpp new file mode 100644 index 0000000000..6228f1cce9 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponent.cpp @@ -0,0 +1,189 @@ + +#include "GpuiHostComponentsCpp.h" + +#include "rn-gpui-sys/src/lib.rs.h" // cxx bridge + +#include +#include + +#include + +#include +#include + +namespace rngpui { +namespace { +/// What is registered while the app starts. Written by the generated files' +/// registration functions, then frozen into `FrozenTable` and never written +/// again. +struct PendingEntries { + std::mutex mutex; + std::vector entries; +}; + +PendingEntries& pending() { + static PendingEntries pending; + return pending; +} + +/// Set by the first registration, before anything can ask for a component, so a +/// hot path skips everything below with one relaxed load. +std::atomic& anyFlag() { + static std::atomic flag{false}; + return flag; +} + +/// The frozen lookup: entries sorted by handle, found by binary search. A few +/// entries, one cache line or two, no hashing and no lock. +struct FrozenTable { + std::vector entries; + std::vector> byHandle; +}; + +std::atomic& lookupCount() { + static std::atomic count{0}; + return count; +} + +std::atomic& frozenTable() { + static std::atomic table{nullptr}; + return table; +} +} // namespace + +void registerHostComponentEntry(const HostComponentEntry& entry) { + if (frozenTable().load(std::memory_order_acquire) != nullptr) { + LOG(ERROR) << "[host-component] " << entry.name + << " registered after the registry froze; ignored"; + return; + } + auto& reg = pending(); + std::lock_guard lock(reg.mutex); + if (reg.entries.size() <= entry.index) { + reg.entries.resize(entry.index + 1); + } + reg.entries[entry.index] = entry; + anyFlag().store(true, std::memory_order_release); +} + +bool anyHostComponents() { + return anyFlag().load(std::memory_order_relaxed); +} + +uint64_t hostEntryLookups() { + return lookupCount().load(std::memory_order_relaxed); +} + +void resetHostEntryLookups() { + lookupCount().store(0, std::memory_order_relaxed); +} + +const HostComponentEntry* hostComponentEntry( + facebook::react::ComponentHandle handle) { + lookupCount().fetch_add(1, std::memory_order_relaxed); + const FrozenTable* table = frozenTable().load(std::memory_order_acquire); + if (table == nullptr) { + return nullptr; + } + const auto& keys = table->byHandle; + auto found = std::lower_bound( + keys.begin(), keys.end(), handle, [](const auto& key, auto value) { + return key.first < value; + }); + if (found == keys.end() || found->first != handle) { + return nullptr; + } + return &table->entries[found->second]; +} + +void registerHostComponentDescriptors( + facebook::react::ComponentDescriptorProviderRegistry& registry) { + if (frozenTable().load(std::memory_order_acquire) != nullptr) { + return; + } + auto table = std::make_unique(); + { + auto& reg = pending(); + std::lock_guard lock(reg.mutex); + table->entries = reg.entries; + } + for (uint32_t index = 0; index < table->entries.size(); ++index) { + const auto& entry = table->entries[index]; + if (entry.provider == nullptr) { + continue; + } + registry.add(entry.provider()); + table->byHandle.emplace_back(entry.handle, index); + } + std::sort(table->byHandle.begin(), table->byHandle.end()); + frozenTable().store(table.release(), std::memory_order_release); +} + +void releaseHostPropsHandle(std::size_t handle) { + delete reinterpret_cast(handle); +} + +void releaseHostEmitterHandle(std::size_t handle) { + delete reinterpret_cast(handle); +} + +void dispatchHostCommand( + int32_t tag, + uint32_t command, + std::vector&& args) { + rust::Vec converted; + converted.reserve(args.size()); + for (auto& value : args) { + HostCommandArg arg; + arg.kind = static_cast(value.kind); + arg.number = value.number; + arg.text = rust::String(value.text); + for (double number : value.numbers) { + arg.numbers.push_back(number); + } + for (const auto& text : value.texts) { + arg.texts.push_back(rust::String(text)); + } + converted.push_back(std::move(arg)); + } + rn_gpui_on_host_command(tag, command, std::move(converted)); +} +} // namespace rngpui + +namespace { +using ImageSource = facebook::react::ImageSource; + +RnGpuiStr borrow(const std::string& text) { + return RnGpuiStr{reinterpret_cast(text.data()), text.size()}; +} +} // namespace + +extern "C" { +RnGpuiStr rn_gpui_image_source_uri(const void* image) { + return borrow(static_cast(image)->uri); +} + +RnGpuiStr rn_gpui_image_source_bundle(const void* image) { + return borrow(static_cast(image)->bundle); +} + +float rn_gpui_image_source_scale(const void* image) { + return static_cast(image)->scale; +} + +float rn_gpui_image_source_width(const void* image) { + return static_cast(image)->size.width; +} + +float rn_gpui_image_source_height(const void* image) { + return static_cast(image)->size.height; +} + +RnGpuiStr rn_gpui_image_source_body(const void* image) { + return borrow(static_cast(image)->body); +} + +RnGpuiStr rn_gpui_image_source_method(const void* image) { + return borrow(static_cast(image)->method); +} +} // extern "C" diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponent.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponent.h new file mode 100644 index 0000000000..814ce924c9 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponent.h @@ -0,0 +1,42 @@ +#pragma once + +#include "rust/cxx.h" + +#include +#include +#include + +namespace rngpui { +class GpuiMountingManager; + +/// Names the mounting manager host component reports are pushed through. +void setHostComponentStateManager(std::weak_ptr manager); + +/// Pushes the size a host component measured into its shadow node's state, +/// which is what Yoga's measure function reads, and asks for the commit that +/// re-measures it. A size the state already holds is dropped there. +void reportHostComponentSize(int32_t tag, float width, float height); + +/// Pushes the insets a container reported for its chrome into its shadow node's +/// state, where `adopt` turns them into Yoga padding around the React children. +/// Insets the state already holds are dropped there. +void reportHostComponentInsets( + int32_t tag, + float top, + float right, + float bottom, + float left); + +/// Releases a props handle a mount op gave Rust. A handle is a heap +/// `shared_ptr` holder, so releasing it drops one reference. +void releaseHostPropsHandle(std::size_t handle); + +/// Releases an event emitter handle a `Create` op gave Rust. +void releaseHostEmitterHandle(std::size_t handle); + +/// How many times the registry was asked for a component, for the test driver's +/// counters. Counted only on the path an app with host components takes: an app +/// without any never reaches the lookup. +uint64_t hostEntryLookups(); +void resetHostEntryLookups(); +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponentsCpp.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponentsCpp.h new file mode 100644 index 0000000000..0bbf826369 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHostComponentsCpp.h @@ -0,0 +1,288 @@ +#pragma once + +#include "GpuiHostComponent.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { +class ComponentDescriptorProviderRegistry; +} + +/// A borrowed C++ `std::string`: bytes and an explicit length. +struct RnGpuiStr { + const uint8_t* data; + size_t len; +}; + +/// A borrowed C++ `std::vector` of plain data. +struct RnGpuiSlice { + const void* data; + size_t len; +}; + +/// A C++ `SharedColor`: `present` is false when React left the color unset. +struct RnGpuiOptU32 { + uint32_t value; + bool present; +}; + +static_assert(sizeof(RnGpuiStr) == 2 * sizeof(void*)); +static_assert(offsetof(RnGpuiStr, data) == 0); +static_assert(offsetof(RnGpuiStr, len) == sizeof(void*)); +static_assert(sizeof(RnGpuiSlice) == 2 * sizeof(void*)); +static_assert(offsetof(RnGpuiSlice, data) == 0); +static_assert(offsetof(RnGpuiSlice, len) == sizeof(void*)); +static_assert(sizeof(RnGpuiOptU32) == 8); +static_assert(offsetof(RnGpuiOptU32, value) == 0); +static_assert(offsetof(RnGpuiOptU32, present) == 4); + +namespace rngpui { +/// The state every host component keeps: its intrinsic size, and the insets a +/// container reports for its chrome. +struct GpuiHostStateData { + float width = 0.f; + float height = 0.f; + /// Whether a size was ever reported. A component that never measures keeps + /// Yoga's own sizing. + bool measured = false; + /// Padding the component's chrome needs around React's children, reported + /// by a container host component. + float insetLeft = 0.f; + float insetTop = 0.f; + float insetRight = 0.f; + float insetBottom = 0.f; +}; + +/// What a props handle owns: one reference to the node's Fabric props. +struct HostPropsHandle { + std::shared_ptr props; +}; + +/// What an emitter handle owns: one reference to the node's event emitter. +struct HostEmitterHandle { + std::shared_ptr emitter; +}; + +/// The handle for `props`, as the address Rust owns. Zero for a null pointer. +inline std::size_t makeHostPropsHandle( + const std::shared_ptr& props) { + return props ? reinterpret_cast(new HostPropsHandle{props}) : 0; +} + +/// The handle for `emitter`, as the address Rust owns. Zero for a null pointer. +inline std::size_t makeHostEmitterHandle( + const std::shared_ptr& emitter) { + return emitter ? reinterpret_cast(new HostEmitterHandle{emitter}) + : 0; +} + +/// The props a generated accessor reads: the handle's props, as the component's +/// own generated props class. The handle was made from a node of that component +/// (the registry index on the op says so), so the downcast is exact. +template +inline const P& hostProps(const void* handle) { + return static_cast( + *static_cast(handle)->props); +} + +/// The emitter a generated event shim dispatches through. +template +inline const E& hostEmitter(const void* handle) { + return static_cast( + *static_cast(handle)->emitter); +} + +/// One command argument, already validated against the spec by the generated +/// dispatcher. Mirrors `HostCommandArg` in the bridge, without cxx types, so +/// the generated file does not need the bridge header. +struct HostCommandValue { + enum Kind : uint8_t { + Number = 0, + Bool = 1, + String = 2, + Numbers = 3, + Bools = 4, + Strings = 5, + }; + Kind kind = Number; + double number = 0; + std::string text; + std::vector numbers; + std::vector texts; +}; + +/// Sends a validated command to the Rust host component of node `tag`, as the +/// `command`-th entry of its spec's `COMMANDS`. +void dispatchHostCommand( + int32_t tag, + uint32_t command, + std::vector&& args); + +/// A leaf host component: its measure function returns the size in its state, +/// when it declares itself `measured`. +template +class GpuiHostLeafShadowNode final + : public facebook::react:: + ConcreteViewShadowNode { + using Base = + facebook::react::ConcreteViewShadowNode; + + public: + static facebook::react::ShadowNodeTraits BaseTraits() { + auto traits = Base::BaseTraits(); + traits.set(facebook::react::ShadowNodeTraits::Trait::LeafYogaNode); + if constexpr (Measured) { + traits.set(facebook::react::ShadowNodeTraits::Trait::MeasurableYogaNode); + } + return traits; + } + + GpuiHostLeafShadowNode( + const facebook::react::ShadowNodeFragment& fragment, + const facebook::react::ShadowNodeFamily::Shared& family, + facebook::react::ShadowNodeTraits traits) + : Base(fragment, family, traits) { + initialize(); + } + + GpuiHostLeafShadowNode( + const facebook::react::ShadowNode& sourceShadowNode, + const facebook::react::ShadowNodeFragment& fragment) + : Base(sourceShadowNode, fragment) { + initialize(); + } + + /// The size the node's state holds, clamped to the constraints. + facebook::react::Size measureContent( + const facebook::react::LayoutContext& /*layoutContext*/, + const facebook::react::LayoutConstraints& layoutConstraints) + const override { + const auto& stateData = this->getStateData(); + float width = stateData.width; + float height = stateData.height; + if (!stateData.measured) { + width = layoutConstraints.minimumSize.width; + height = layoutConstraints.minimumSize.height; + } + width = std::clamp( + width, + layoutConstraints.minimumSize.width, + layoutConstraints.maximumSize.width); + height = std::clamp( + height, + layoutConstraints.minimumSize.height, + layoutConstraints.maximumSize.height); + return facebook::react::Size{width, height}; + } + + private: + void initialize() noexcept { + if constexpr (Measured) { + this->enableMeasurement(); + } + } +}; + +/// A container host component: it draws chrome *around* React children. +template +class GpuiHostContainerShadowNode final + : public facebook::react:: + ConcreteViewShadowNode { + public: + using facebook::react::ConcreteViewShadowNode:: + ConcreteViewShadowNode; +}; + +/// The descriptor of a container: applies the insets the component reported for +/// its chrome as real Yoga padding, so its React children are laid out inside +/// it. +template +class GpuiHostContainerDescriptor final + : public facebook::react::ConcreteComponentDescriptor { + public: + using facebook::react::ConcreteComponentDescriptor< + ShadowNodeT>::ConcreteComponentDescriptor; + + void adopt(facebook::react::ShadowNode& shadowNode) const override { + auto& layoutable = + static_cast(shadowNode); + const auto& state = + static_cast&>( + *shadowNode.getState()); + const auto& data = state.getData(); + layoutable.setPadding(facebook::react::RectangleEdges{ + data.insetLeft, data.insetTop, data.insetRight, data.insetBottom}); + facebook::react::ConcreteComponentDescriptor::adopt( + shadowNode); + } +}; + +/// What the generated C++ file knows about one component, registered at +/// startup. +struct HostComponentEntry { + uint32_t index = 0; + const char* name = ""; + /// `Name::Handle()` of the component's shadow node: what a `ShadowView` + /// carries, and the key of the table lookup. + facebook::react::ComponentHandle handle = 0; + bool measured = false; + bool container = false; + bool nativeSurface = false; + /// Builds the Fabric descriptor provider for the component. + facebook::react::ComponentDescriptorProvider (*provider)() = nullptr; + /// Compares two props objects of this component and sets one bit per changed + /// prop in `words`, which holds `changeWords` zeroed words. + void (*changed)( + const facebook::react::Props& before, + const facebook::react::Props& after, + uint64_t* words) = nullptr; + uint32_t changeWords = 1; + /// Validates a command's arguments against the spec and dispatches it. + /// Returns whether the name is one of the spec's commands. + bool (*command)( + int32_t tag, + const std::string& name, + const folly::dynamic& args) = nullptr; +}; + +/// Registers a component. Called from the generated file's +/// `rn_gpui_register_host_components_()`, before the first surface +/// starts. Registering after the registry froze is an error. +void registerHostComponentEntry(const HostComponentEntry& entry); + +/// Whether any host component was ever registered, as one relaxed load. Every +/// per-op check goes through this first, so an app with no host components pays +/// one predictable branch. +bool anyHostComponents(); + +/// The entry for the component `handle` names, or null. Lock-free: the table is +/// built once, when the descriptors are registered, and never changes. +const HostComponentEntry* hostComponentEntry( + facebook::react::ComponentHandle handle); + +/// Adds the descriptor provider of every registered component to Fabric's +/// provider registry, and freezes the registry. +void registerHostComponentDescriptors( + facebook::react::ComponentDescriptorProviderRegistry& registry); +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpBridge.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpBridge.cpp new file mode 100644 index 0000000000..11ff49d808 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpBridge.cpp @@ -0,0 +1,193 @@ +#include "GpuiHttpBridge.h" + +#include + +// The completion callback's declaration, exactly as cxx emits it. +// +// Hand-writing it is what broke the Windows build: this file has to declare +// `rngpui::rn_gpui_http_complete` so the call below links, and a hand-written +// version without `noexcept` is a different function when MSVC sees both in one +// unity translation unit — C2382. macOS and Linux compile this file alone and +// never met the generated declaration, so only Windows refused. +#include "rn-gpui-sys/src/lib.rs.h" // rn_gpui_http_complete + +#include +#include +#include +#include +#include +#include +#include + +#include "GpuiHttpClient.h" + +namespace rngpui { + +namespace { + +/// The HTTP interface lives under `facebook::react`, like the rest of the C++ +/// runtime; the alias keeps the body readable. +namespace http = facebook::react::http; +using facebook::react::IHttpClient; + +/// One response, assembled from the client's callbacks. +struct Result { + int status{0}; + http::Headers headers; + std::string body; + std::string error; + bool done{false}; +}; + +/// The handle a cancelled request is found by. +struct InFlight { + std::atomic cancelled{false}; +}; + +std::mutex g_requestsMutex; +std::map> g_requests; + +/// "name: value" lines, newline separated. One string rather than a cxx struct: +/// header maps are small and this crosses the boundary rarely. +http::Headers parseHeaders(const std::string& text) { + http::Headers headers; + std::size_t start = 0; + while (start <= text.size() && !text.empty()) { + const auto end = text.find('\n', start); + const auto line = text.substr( + start, end == std::string::npos ? std::string::npos : end - start); + const auto colon = line.find(':'); + if (colon != std::string::npos) { + std::string value = line.substr(colon + 1); + while (!value.empty() && + (value.front() == ' ' || value.front() == '\t')) { + value.erase(value.begin()); + } + while (!value.empty() && (value.back() == '\r' || value.back() == ' ')) { + value.pop_back(); + } + // `Headers` is a vector of pairs, not a map: React Native preserves the + // order the server sent, and a map would sort them. + headers.emplace_back(line.substr(0, colon), std::move(value)); + } + if (end == std::string::npos) { + break; + } + start = end + 1; + } + return headers; +} + +std::string serializeHeaders(const http::Headers& headers) { + std::string text; + for (const auto& [name, value] : headers) { + if (!text.empty()) { + text.push_back('\n'); + } + text += name; + text += ": "; + text += value; + } + return text; +} + +} // namespace + +void rn_gpui_http_request( + std::uint64_t request_id, + ::rust::Str method, + ::rust::Str url, + ::rust::Str headers, + ::rust::Str body) { + auto state = std::make_shared(); + { + std::lock_guard lock(g_requestsMutex); + g_requests[request_id] = state; + } + + const std::string methodText(method.data(), method.size()); + const std::string urlText(url.data(), url.size()); + const std::string headersText(headers.data(), headers.size()); + const std::string bodyText(body.data(), body.size()); + + std::thread( + [request_id, state, methodText, urlText, headersText, bodyText]() { + auto result = std::make_shared(); + auto finished = std::make_shared(); + auto condition = std::make_shared(); + + http::NetworkCallbacks callbacks; + callbacks.onResponse = [result]( + uint16_t code, + http::Headers responseHeaders, + std::string /*responseUrl*/) { + result->status = code; + result->headers = std::move(responseHeaders); + }; + callbacks.onBody = [result](std::unique_ptr data) { + if (!data) { + return; + } + for (const auto& range : *data) { + result->body.append( + reinterpret_cast(range.data()), range.size()); + } + }; + callbacks.onResponseComplete = [result, finished, condition]( + std::string error, + bool /*timeoutError*/) { + result->error = std::move(error); + { + std::lock_guard lock(*finished); + result->done = true; + } + condition->notify_all(); + }; + + // One client per request: `CurlHttpClient` is stateless, and the + // cookies it shares live in a process-wide handle created on first use. + auto client = gpui_http_client_factory()(); + client->sendRequest( + std::move(callbacks), + methodText, + urlText, + parseHeaders(headersText), + http::Body{.string = bodyText}); + + // `sendRequest` returns as soon as the transfer has been started on its + // own thread, so the response is awaited here rather than assumed. + { + std::unique_lock lock(*finished); + condition->wait(lock, [&result] { return result->done; }); + } + { + std::lock_guard lock(g_requestsMutex); + g_requests.erase(request_id); + } + if (state->cancelled.load(std::memory_order_relaxed)) { + return; + } + const auto serialized = serializeHeaders(result->headers); + // `rust::Slice` for the body: it is raw response bytes, and an image + // response is not UTF-8. + rn_gpui_http_complete( + request_id, + result->status, + ::rust::Str(serialized.data(), serialized.size()), + ::rust::Slice( + reinterpret_cast(result->body.data()), + result->body.size()), + ::rust::Str(result->error.data(), result->error.size())); + }) + .detach(); +} + +void rn_gpui_http_request_cancel(std::uint64_t request_id) { + std::lock_guard lock(g_requestsMutex); + const auto it = g_requests.find(request_id); + if (it != g_requests.end()) { + it->second->cancelled.store(true, std::memory_order_relaxed); + } +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpBridge.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpBridge.h new file mode 100644 index 0000000000..83d3d15056 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpBridge.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "rust/cxx.h" + +namespace rngpui { + +// One request, issued for GPUI's image, font and remote vector image loading. +// +// GPUI's `HttpClient` is synchronous from the caller's point of view: it +// returns a future that resolves when the response is complete. React Native's +// client is callback-shaped, so this runs the request on its own thread and +// reports the whole response at the end. `request_id` is echoed back on the +// Rust side. +// +// The request counts in `rn_gpui_http_requests_in_flight` for its lifetime, so +// the test driver's idle detection covers image and font loads too. +void rn_gpui_http_request( + std::uint64_t request_id, + rust::Str method, + rust::Str url, + rust::Str headers, + rust::Str body); + +// Cancels an in-flight request, if it is still running. +void rn_gpui_http_request_cancel(std::uint64_t request_id); + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpClient.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpClient.cpp new file mode 100644 index 0000000000..f8d0b0801d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpClient.cpp @@ -0,0 +1,563 @@ +#include "GpuiHttpClient.h" +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rngpui { +namespace { +using namespace facebook::react; + +// curl_global_init() is not thread-safe and has to run before any other libcurl +// call, so every client construction goes through call_once(). +void ensureCurlInitialized() { + static std::once_flag once; + std::call_once(once, []() { curl_global_init(CURL_GLOBAL_DEFAULT); }); +} + +struct SharedCookies { + CURLSH* handle{curl_share_init()}; + std::mutex mutex; + + SharedCookies() { + curl_share_setopt(handle, CURLSHOPT_LOCKFUNC, lock); + curl_share_setopt(handle, CURLSHOPT_UNLOCKFUNC, unlock); + curl_share_setopt(handle, CURLSHOPT_USERDATA, this); + curl_share_setopt(handle, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); + } + ~SharedCookies() { + curl_share_cleanup(handle); + } + + static void lock(CURL*, curl_lock_data, curl_lock_access, void* data) { + static_cast(data)->mutex.lock(); + } + static void unlock(CURL*, curl_lock_data, void* data) { + static_cast(data)->mutex.unlock(); + } +}; + +SharedCookies& sharedCookies() { + static SharedCookies cookies; + return cookies; +} + +// Shared by the worker thread and the token handed back to the caller: cancel() +// can be called from any thread at any time. +struct Cancellation { + std::atomic cancelled{false}; +}; + +std::atomic httpRequestsInFlight{0}; +std::mutex httpRequestsMutex; +std::unordered_map httpRequestUrlsInFlight; + +struct RequestActivity { + explicit RequestActivity(const std::string& url) : url_(url) { + httpRequestsInFlight.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(httpRequestsMutex); + ++httpRequestUrlsInFlight[url_]; + } + ~RequestActivity() { + httpRequestsInFlight.fetch_sub(1, std::memory_order_relaxed); + std::lock_guard lock(httpRequestsMutex); + auto it = httpRequestUrlsInFlight.find(url_); + if (it != httpRequestUrlsInFlight.end() && --it->second == 0) { + httpRequestUrlsInFlight.erase(it); + } + } + + private: + std::string url_; +}; + +class CurlRequestToken final : public http::IRequestToken { + public: + explicit CurlRequestToken(std::shared_ptr cancellation) + : cancellation_(std::move(cancellation)) {} + + void cancel() noexcept override { + cancellation_->cancelled.store(true, std::memory_order_relaxed); + } + + private: + std::shared_ptr cancellation_; +}; + +struct Transfer { + std::shared_ptr cancellation; + http::NetworkCallbacks callbacks; + std::string body; + http::Headers responseHeaders; + int64_t receivedBytes{0}; + int64_t totalBytes{-1}; + int64_t uploadedBytes{0}; + int64_t totalUploadBytes{-1}; +}; + +bool isCancelled(const Transfer& transfer) { + return transfer.cancellation->cancelled.load(std::memory_order_relaxed); +} + +// Returning a short count makes libcurl abort the transfer with +// CURLE_WRITE_ERROR, which is how cancellation interrupts a download in +// progress. +size_t writeCallback(char* data, size_t size, size_t count, void* userData) { + auto& transfer = *static_cast(userData); + if (isCancelled(transfer)) { + return 0; + } + + const auto length = static_cast(size * count); + + // `sendIncrementalUpdates` asks for the body as it arrives instead of one + // buffer at the end; the two delivery styles are mutually exclusive. + if (transfer.callbacks.sendIncrementalUpdates && + transfer.callbacks.onBodyIncremental) { + const int64_t consumed = transfer.callbacks.onBodyIncremental( + transfer.receivedBytes, + transfer.totalBytes, + folly::IOBuf::copyBuffer(data, static_cast(length))); + if (consumed < length) { + return 0; + } + } else { + transfer.body.append(data, static_cast(length)); + } + + transfer.receivedBytes += length; + if (transfer.callbacks.sendProgressUpdates && + transfer.callbacks.onBodyProgress) { + transfer.callbacks.onBodyProgress( + transfer.receivedBytes, transfer.totalBytes); + } + return size * count; +} + +// libcurl hands the header block over one line at a time, status line first and +// a blank line last. +size_t headerCallback(char* data, size_t size, size_t count, void* userData) { + auto& transfer = *static_cast(userData); + const size_t length = size * count; + const std::string_view line(data, length); + + if (isCancelled(transfer)) { + return 0; + } + if (line.starts_with("HTTP/")) { + // libcurl reports each redirect/interim response header block. Only the + // final response headers belong on the Fetch Response. + transfer.responseHeaders.clear(); + } + if (line == "\r\n" || line == "\n") { + return length; // End of the response header block. + } + + const size_t colon = line.find(':'); + if (colon != std::string_view::npos) { + std::string_view name = line.substr(0, colon); + std::string_view value = line.substr(colon + 1); + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.remove_prefix(1); + } + while ( + !value.empty() && + (value.back() == '\r' || value.back() == '\n' || value.back() == ' ')) { + value.remove_suffix(1); + } + transfer.responseHeaders.emplace_back( + std::string(name), std::string(value)); + } + return length; +} + +// The callback that keeps cancellation responsive between writes and, as a +// side effect, tells us the expected body size for progress reporting. +int progressCallback( + void* userData, + curl_off_t dltotal, + curl_off_t /*dlnow*/, + curl_off_t ultotal, + curl_off_t ulnow) { + auto& transfer = *static_cast(userData); + if (dltotal > 0) { + transfer.totalBytes = static_cast(dltotal); + } + if (ultotal >= 0) { + transfer.totalUploadBytes = static_cast(ultotal); + } + if (ulnow >= 0 && static_cast(ulnow) != transfer.uploadedBytes) { + transfer.uploadedBytes = static_cast(ulnow); + if (transfer.callbacks.onUploadProgress) { + transfer.callbacks.onUploadProgress( + transfer.uploadedBytes, transfer.totalUploadBytes); + } + } + return isCancelled(transfer) ? 1 : 0; // Non-zero aborts the transfer. +} + +struct CurlHandle { + CURL* handle{nullptr}; + curl_slist* headers{nullptr}; + curl_mime* mime{nullptr}; + + ~CurlHandle() { + if (mime != nullptr) { + curl_mime_free(mime); + } + curl_slist_free_all(headers); + if (handle != nullptr) { + curl_easy_cleanup(handle); + } + } +}; + +std::optional decodeBase64(std::string_view input) { + static constexpr std::string_view alphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string result; + uint32_t value = 0; + int bits = -8; + for (char ch : input) { + if (ch == '=') { + break; + } + if (ch == ' ' || ch == '\r' || ch == '\n' || ch == '\t') { + continue; + } + const auto index = alphabet.find(ch); + if (index == std::string_view::npos) { + return std::nullopt; + } + value = (value << 6) | static_cast(index); + bits += 6; + if (bits >= 0) { + result.push_back(static_cast((value >> bits) & 0xff)); + bits -= 8; + } + } + return result; +} + +bool addFormData(curl_mime* mime, const http::FormData& fields) { + for (const auto& field : fields) { + auto* part = curl_mime_addpart(mime); + curl_mime_name(part, field.fieldName.c_str()); + std::string filename; + for (const auto& [name, value] : field.headers) { + if (name == "Content-Type" || name == "content-type") { + curl_mime_type(part, value.c_str()); + } else if ( + name == "Content-Disposition" || name == "content-disposition") { + const auto filenamePos = value.find("filename=\""); + if (filenamePos != std::string::npos) { + const auto start = filenamePos + 10; + const auto end = value.find('"', start); + if (end != std::string::npos) { + filename = value.substr(start, end - start); + } + } + } + } + if (field.string) { + curl_mime_data(part, field.string->data(), field.string->size()); + } else if (field.uri) { + std::string path = *field.uri; + if (path.starts_with("file://")) { + path.erase(0, 7); + } + if (curl_mime_filedata(part, path.c_str()) != CURLE_OK) { + return false; + } + if (!filename.empty()) { + curl_mime_filename(part, filename.c_str()); + } + } + } + return true; +} + +void performRequest( + Transfer transfer, + const std::string& method, + const std::string& url, + const http::Headers& requestHeaders, + const http::Body& requestBody, + uint32_t timeout) { + const RequestActivity activity(url); + const auto complete = [&transfer](std::string error, bool timeoutError) { + if (transfer.callbacks.onResponseComplete) { + transfer.callbacks.onResponseComplete(std::move(error), timeoutError); + } + }; + + if (url.starts_with("blob:")) { + try { + const folly::Uri uri(url); + auto path = uri.path(); + while (!path.empty() && path.front() == '/') { + path.erase(path.begin()); + } + uint64_t offset = 0; + uint64_t size = std::numeric_limits::max(); + const auto query = uri.query(); + const auto offsetKey = query.find("offset="); + if (offsetKey != std::string::npos) { + offset = std::stoull(query.substr(offsetKey + 7)); + } + const auto sizeKey = query.find("size="); + if (sizeKey != std::string::npos) { + size = std::stoull(query.substr(sizeKey + 5)); + } + auto data = readBlob(path, offset, size); + if (!data) { + if (transfer.callbacks.onResponse) { + transfer.callbacks.onResponse(404, {}, url); + } + complete("Blob URL is no longer available", false); + return; + } + if (transfer.callbacks.onResponse) { + transfer.callbacks.onResponse( + 200, + {{"Content-Length", std::to_string(data->size())}, + {"Content-Type", "application/octet-stream"}}, + url); + } + if (transfer.callbacks.sendIncrementalUpdates && + transfer.callbacks.onBodyIncremental) { + transfer.callbacks.onBodyIncremental( + 0, + static_cast(data->size()), + folly::IOBuf::copyBuffer(*data)); + } else if (transfer.callbacks.onBody) { + transfer.callbacks.onBody(folly::IOBuf::copyBuffer(*data)); + } + complete("", false); + } catch (const std::exception& error) { + complete(std::string("Invalid Blob URL: ") + error.what(), false); + } + return; + } + + CurlHandle curl; + curl.handle = curl_easy_init(); + if (curl.handle == nullptr) { + complete("libcurl: could not create an easy handle", false); + return; + } + + for (const auto& [name, value] : requestHeaders) { + const std::string line = name + ": " + value; + curl.headers = curl_slist_append(curl.headers, line.c_str()); + } + + curl_easy_setopt(curl.handle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl.handle, CURLOPT_USERAGENT, "react-native-gpui/0.1"); + curl_easy_setopt(curl.handle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl.handle, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl.handle, CURLOPT_ACCEPT_ENCODING, ""); + curl_easy_setopt(curl.handle, CURLOPT_SHARE, sharedCookies().handle); + if (requestBody.withCredentials) { + // Enables libcurl's in-memory cookie engine. Cookie data is shared between + // requests for the lifetime of the process and is never written to disk. + curl_easy_setopt(curl.handle, CURLOPT_COOKIEFILE, ""); + } + curl_easy_setopt(curl.handle, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl.handle, CURLOPT_WRITEDATA, &transfer); + curl_easy_setopt(curl.handle, CURLOPT_HEADERFUNCTION, headerCallback); + curl_easy_setopt(curl.handle, CURLOPT_HEADERDATA, &transfer); + curl_easy_setopt(curl.handle, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(curl.handle, CURLOPT_XFERINFOFUNCTION, progressCallback); + curl_easy_setopt(curl.handle, CURLOPT_XFERINFODATA, &transfer); + if (curl.headers != nullptr) { + curl_easy_setopt(curl.handle, CURLOPT_HTTPHEADER, curl.headers); + } + if (timeout > 0) { + // The interface documents a timeout in seconds, 0 meaning "no timeout". + curl_easy_setopt(curl.handle, CURLOPT_TIMEOUT, static_cast(timeout)); + } + if (method != "GET" && method != "get") { + curl_easy_setopt(curl.handle, CURLOPT_CUSTOMREQUEST, method.c_str()); + } + std::optional requestBodyBlob; + if (requestBody.formData) { + curl.mime = curl_mime_init(curl.handle); + if (curl.mime == nullptr || + !addFormData(curl.mime, *requestBody.formData)) { + complete("Invalid FormData request body", false); + return; + } + curl_easy_setopt(curl.handle, CURLOPT_MIMEPOST, curl.mime); + } else if (requestBody.blob) { + auto blob = readBlob( + requestBody.blob->blobId, + requestBody.blob->offset, + requestBody.blob->size); + if (!blob) { + complete("Blob request body is no longer available", false); + return; + } + requestBodyBlob = std::move(*blob); + curl_easy_setopt(curl.handle, CURLOPT_POSTFIELDS, requestBodyBlob->data()); + curl_easy_setopt( + curl.handle, + CURLOPT_POSTFIELDSIZE_LARGE, + static_cast(requestBodyBlob->size())); + } else if (requestBody.base64) { + auto decoded = decodeBase64(*requestBody.base64); + if (!decoded) { + complete("Invalid base64 request body", false); + return; + } + curl_easy_setopt(curl.handle, CURLOPT_POSTFIELDS, decoded->data()); + curl_easy_setopt( + curl.handle, + CURLOPT_POSTFIELDSIZE_LARGE, + static_cast(decoded->size())); + } else if (requestBody.string.has_value()) { + curl_easy_setopt( + curl.handle, CURLOPT_POSTFIELDS, requestBody.string->data()); + curl_easy_setopt( + curl.handle, + CURLOPT_POSTFIELDSIZE_LARGE, + static_cast(requestBody.string->size())); + } + + const CURLcode result = curl_easy_perform(curl.handle); + + long responseCode = 0; + char* effectiveUrlValue = nullptr; + curl_easy_getinfo(curl.handle, CURLINFO_RESPONSE_CODE, &responseCode); + curl_easy_getinfo(curl.handle, CURLINFO_EFFECTIVE_URL, &effectiveUrlValue); + const std::string effectiveUrl = + effectiveUrlValue == nullptr ? url : effectiveUrlValue; + curl_easy_cleanup(curl.handle); + curl.handle = nullptr; + + if (isCancelled(transfer)) { + complete("Request cancelled", false); + return; + } + if (result != CURLE_OK) { + const auto error = curl_easy_strerror(result); + LOG(ERROR) << "[gpui-http] " << method << " " << url + << " failed: " << error; + complete(error, result == CURLE_OPERATION_TIMEDOUT); + return; + } + + const auto statusCode = static_cast(responseCode); + if (transfer.callbacks.onResponse) { + transfer.callbacks.onResponse( + statusCode, transfer.responseHeaders, effectiveUrl); + } + if (transfer.callbacks.onBody && !transfer.callbacks.sendIncrementalUpdates) { + transfer.callbacks.onBody(folly::IOBuf::copyBuffer(transfer.body)); + } + complete("", false); +} + +class CurlHttpClient final : public IHttpClient { + public: + std::unique_ptr sendRequest( + http::NetworkCallbacks&& callbacks, + const std::string& method, + const std::string& url, + const http::Headers& headers = {}, + const http::Body& body = {}, + uint32_t timeout = 0, + std::optional /*loggingId*/ = std::nullopt) override { + ensureCurlInitialized(); + + auto cancellation = std::make_shared(); + Transfer transfer; + transfer.cancellation = cancellation; + transfer.callbacks = std::move(callbacks); + + // The request runs to completion on its own thread: callers block on a + // future that these callbacks resolve, so the request must not occupy the + // calling thread. + std::thread( + performRequest, + std::move(transfer), + method, + url, + headers, + body, + timeout) + .detach(); + + return std::make_unique(std::move(cancellation)); + } +}; + +} // namespace + +extern "C" uint64_t rn_gpui_http_requests_in_flight() noexcept { + return httpRequestsInFlight.load(std::memory_order_relaxed); +} + +extern "C" uint64_t rn_gpui_http_requests_not_ignored( + const char* newline_separated_patterns) noexcept { + std::lock_guard lock(httpRequestsMutex); + uint64_t count = 0; + for (const auto& [url, active] : httpRequestUrlsInFlight) { + bool ignored = false; + if (newline_separated_patterns != nullptr) { + const std::string patterns(newline_separated_patterns); + size_t start = 0; + while (start <= patterns.size()) { + const auto end = patterns.find('\n', start); + const auto pattern = patterns.substr(start, end - start); + if (!pattern.empty() && url.find(pattern) != std::string::npos) { + ignored = true; + break; + } + if (end == std::string::npos) + break; + start = end + 1; + } + } + if (!ignored) + count += active; + } + return count; +} + +HttpClientFactory gpui_http_client_factory() { + return []() -> std::unique_ptr { + return std::make_unique(); + }; +} + +bool gpui_http_client_clear_cookies() { + ensureCurlInitialized(); + CURL* handle = curl_easy_init(); + if (handle == nullptr) { + return false; + } + curl_easy_setopt(handle, CURLOPT_SHARE, sharedCookies().handle); + curl_easy_setopt(handle, CURLOPT_COOKIEFILE, ""); + const auto result = curl_easy_setopt(handle, CURLOPT_COOKIELIST, "ALL"); + curl_easy_cleanup(handle); + return result == CURLE_OK; +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpClient.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpClient.h new file mode 100644 index 0000000000..9208426170 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiHttpClient.h @@ -0,0 +1,15 @@ +// ReactHost's constructor throws unless an HttpClientFactory is in the +// ContextContainer. +#pragma once + +#include +#include + +namespace rngpui { +facebook::react::HttpClientFactory gpui_http_client_factory(); +bool gpui_http_client_clear_cookies(); +extern "C" uint64_t rn_gpui_http_requests_in_flight() noexcept; +extern "C" uint64_t rn_gpui_http_requests_not_ignored( + const char* newline_separated_patterns) noexcept; + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiImageLoader.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiImageLoader.h new file mode 100644 index 0000000000..93cca21d85 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiImageLoader.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include "rust/cxx.h" + +namespace rngpui { +void rn_gpui_on_image_load_result( + std::uint64_t request_id, + double width, + double height, + rust::String error); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.cpp new file mode 100644 index 0000000000..346ceb96d9 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.cpp @@ -0,0 +1,125 @@ +#include "GpuiJsEngine.h" + +#include + +namespace rngpui { +namespace { +std::mutex& registryMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector& registry() { + static std::vector engines; + return engines; +} + +std::string& selection() { + static std::string name; + return name; +} + +std::string joinNames(const std::vector& engines) { + std::string joined; + for (const auto& engine : engines) { + if (!joined.empty()) { + joined += ", "; + } + joined += engine.info.name; + } + return joined; +} + +[[noreturn]] void throwNoEngine() { + throw std::runtime_error( + "no JS engine installed; add @react-native-gpui/hermes (or another " + "engine package) to the app's dependencies"); +} + +[[noreturn]] void throwUnknownEngine( + const std::string& name, + const std::vector& engines) { + throw std::runtime_error( + "unknown JS engine '" + name + + "'; registered engines: " + joinNames(engines)); +} + +[[noreturn]] void throwAmbiguousEngine( + const std::vector& engines) { + throw std::runtime_error( + "several JS engines are installed (" + joinNames(engines) + + "); choose one with RN_GPUI_JS_ENGINE or DesktopAppConfig::js_engine"); +} + +const GpuiJsEngine* findEngine( + const std::vector& engines, + std::string_view name) { + for (const auto& engine : engines) { + if (engine.info.name == name) { + return &engine; + } + } + return nullptr; +} + +} // namespace + +void registerJsEngine(GpuiJsEngine engine) { + if (engine.info.name.empty()) { + throw std::invalid_argument("a JS engine must report a name"); + } + if (engine.makeRuntimeFactory == nullptr) { + throw std::invalid_argument( + "JS engine '" + engine.info.name + "' has no runtime factory"); + } + std::lock_guard lock(registryMutex()); + if (findEngine(registry(), engine.info.name) != nullptr) { + throw std::invalid_argument( + "duplicate JS engine '" + engine.info.name + "'"); + } + registry().push_back(std::move(engine)); +} + +void selectJsEngine(std::string_view name) { + std::lock_guard lock(registryMutex()); + if (name.empty()) { + selection().clear(); + return; + } + if (findEngine(registry(), name) == nullptr) { + throwUnknownEngine(std::string(name), registry()); + } + selection() = std::string(name); +} + +const GpuiJsEngine& activeJsEngine() { + std::lock_guard lock(registryMutex()); + const auto& engines = registry(); + if (engines.empty()) { + throwNoEngine(); + } + if (!selection().empty()) { + if (const auto* engine = findEngine(engines, selection())) { + return *engine; + } + // The selection can only go stale if the registry changed after startup; + // report it rather than silently running a different engine. + throwUnknownEngine(selection(), engines); + } + if (engines.size() == 1) { + return engines.front(); + } + throwAmbiguousEngine(engines); +} + +std::vector registeredJsEngines() { + std::lock_guard lock(registryMutex()); + std::vector infos; + infos.reserve(registry().size()); + for (const auto& engine : registry()) { + infos.push_back(engine.info); + } + return infos; +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.h new file mode 100644 index 0000000000..f7424beacb --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiJsEngine.h @@ -0,0 +1,94 @@ +// Core has no built-in JS engine: an engine is an installed package that +// registers itself here. +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { +class CallInvoker; +class JSRuntimeFactory; +class TurboModule; +} // namespace facebook::react + +namespace rngpui { +class GpuiFeatureFlagsOverrides + : public facebook::react::ReactNativeFeatureFlagsOverridesOSSStable { + public: + // React Native's C++ AnimatedModule queues operations until + // finishOperationBatch(). GPUI is neither Android nor iOS, so RN's JS helper + // only sends those batch boundaries when this flag is enabled. + bool cxxNativeAnimatedEnabled() override { + return true; + } +}; + +struct GpuiJsEngineInfo { + std::string name; + std::string version; +}; + +// Every capability an engine may provide. Only `info` and `makeRuntimeFactory` +// are required; core checks the optional members before use. +struct GpuiJsEngine { + GpuiJsEngineInfo info; + + // Required. Creates the factory ReactHost uses for every (re)load. + std::function()> + makeRuntimeFactory; + + // Optional. Loading stock Node-API `.node` addons. Creating a napi_env for a + // runtime and loading a shared library into it are VM-specific, so the + // engine supplies a loader; everything else (path resolution, the cache, + // error messages) is core's. When missing, `_requireNodeLibrary` still + // exists and throws an error naming the engine. + struct NodeAddons { + // Called once per runtime, on the JS thread, before the app bundle runs. + // `jsInvoker` schedules work on that same JS thread. Returns a function + // that loads the addon at an absolute path and returns its exports, or + // throws a jsi::JSError with a readable message. + std::function( + std::shared_ptr jsInvoker)> + makeLoader; + }; + std::optional nodeAddons; + + // Optional. Returns the feature-flag overrides core installs. An engine that + // needs flags of its own derives from GpuiFeatureFlagsOverrides and + // overrides only those flags, so core's survive. When unset, core installs + // GpuiFeatureFlagsOverrides itself. + std::function()> + makeFeatureFlagsOverrides; +}; + +// Called by engine packages from their registration function. Throws +// std::invalid_argument for an empty name, a duplicate name, or a missing +// runtime factory. +void registerJsEngine(GpuiJsEngine engine); + +// Used by core. Throws std::runtime_error when no engine is registered, when +// several are registered and none was selected, or when the selected name is +// not registered. +const GpuiJsEngine& activeJsEngine(); + +std::vector registeredJsEngines(); + +// Chooses the engine for this run, before the host starts. An empty name +// returns to the default: the only registered engine, or an error when several +// are registered. +void selectJsEngine(std::string_view name); + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiLogBoxSurfaceDelegate.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiLogBoxSurfaceDelegate.cpp new file mode 100644 index 0000000000..49dac96337 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiLogBoxSurfaceDelegate.cpp @@ -0,0 +1,49 @@ +#include "GpuiLogBoxSurfaceDelegate.h" + +#include "GpuiHost.h" +#include "rn-gpui-sys/src/lib.rs.h" // cxx bridge + +#include + +namespace rngpui { +void GpuiLogBoxSurfaceDelegate::createContentView(std::string appKey) { + appKey_ = std::move(appKey); + ready_ = true; + rn_gpui_log(1, "LogBox content view created (appKey=" + appKey_ + ")"); +} + +bool GpuiLogBoxSurfaceDelegate::isContentViewReady() { + return ready_; +} + +void GpuiLogBoxSurfaceDelegate::destroyContentView() { + if (showing_) { + hide(); + } + ready_ = false; + rn_gpui_log(1, "LogBox content view destroyed"); +} + +void GpuiLogBoxSurfaceDelegate::show() { + if (!ready_ || showing_) { + return; + } + showing_ = true; + rn_gpui_log(1, "LogBox shown"); + host_.start_log_box_surface(); +} + +void GpuiLogBoxSurfaceDelegate::hide() { + if (!showing_) { + return; + } + showing_ = false; + rn_gpui_log(1, "LogBox hidden"); + host_.stop_log_box_surface(); +} + +bool GpuiLogBoxSurfaceDelegate::isShowing() { + return showing_; +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiLogBoxSurfaceDelegate.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiLogBoxSurfaceDelegate.h new file mode 100644 index 0000000000..90d103b006 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiLogBoxSurfaceDelegate.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +namespace rngpui { +class GpuiHost; + +/// React renders LogBox into a second React surface (app key `"LogBox"`). It +/// is shown as an overlay in the main window; `hide()` stops the surface so +/// nothing covers the app while LogBox is closed. +class GpuiLogBoxSurfaceDelegate : public facebook::react::SurfaceDelegate { + public: + explicit GpuiLogBoxSurfaceDelegate(GpuiHost& host) : host_(host) {} + + void createContentView(std::string appKey) override; + bool isContentViewReady() override; + void destroyContentView() override; + void show() override; + void hide() override; + bool isShowing() override; + + private: + GpuiHost& host_; + std::string appKey_; + bool ready_{false}; + bool showing_{false}; +}; + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiModules.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiModules.cpp new file mode 100644 index 0000000000..ed73363457 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiModules.cpp @@ -0,0 +1,80 @@ +#include "GpuiModules.h" + +#include + +namespace rngpui { +namespace { +std::mutex frameRequestsMutex; +std::mutex pendingWorkMutex; +std::unordered_map& pendingWorkByOwner() { + static std::unordered_map work; + return work; +} +} // namespace + +GpuiModuleHooks& gpuiModuleHooks() { + static GpuiModuleHooks hooks; + return hooks; +} + +GpuiExtensionHooks& gpuiExtensionHooks() { + static GpuiExtensionHooks hooks; + return hooks; +} + +void registerComponentProps( + const std::string& componentName, + extension::ComponentPropsFn fn) { + gpuiExtensionHooks().componentProps[componentName] = std::move(fn); +} + +void registerMountObserver(extension::MountObserver observer) { + gpuiExtensionHooks().mountObservers.push_back(std::move(observer)); +} + +void registerWindowInsetsObserver(extension::WindowInsetsFn fn) { + gpuiExtensionHooks().windowInsetsObservers.push_back(std::move(fn)); +} + +void registerFrameCallback(GpuiFrameFn fn) { + gpuiExtensionHooks().frameCallbacks.push_back(std::move(fn)); +} + +void requestFrames(const std::string& owner, bool needsFrames) { + { + std::lock_guard lock(frameRequestsMutex); + gpuiExtensionHooks().frameRequests[owner] = needsFrames; + } + updateFrameDemand(); +} + +bool anyFrameRequested() { + std::lock_guard lock(frameRequestsMutex); + for (const auto& [owner, needsFrames] : gpuiExtensionHooks().frameRequests) { + if (needsFrames) + return true; + } + return false; +} + +void reportPendingWork(const std::string& owner, uint64_t count) { + std::lock_guard lock(pendingWorkMutex); + if (count == 0) + pendingWorkByOwner().erase(owner); + else + pendingWorkByOwner()[owner] = count; +} + +uint64_t pendingWork() { + std::lock_guard lock(pendingWorkMutex); + uint64_t total = 0; + for (const auto& [owner, count] : pendingWorkByOwner()) + total += count; + return total; +} + +void registerPointerObserver(extension::PointerObserverFn fn) { + gpuiExtensionHooks().pointerObservers.push_back(std::move(fn)); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiModules.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiModules.h new file mode 100644 index 0000000000..dc8f750d98 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiModules.h @@ -0,0 +1,160 @@ +// Code that depends on an optional package is a module: it lives in the +// package itself, under `desktop/cpp/`, and is compiled only when that package +// is installed. A module declares itself under `platforms.desktop` in its +// react-native.config.js. The platform's own modules live in cpp/modules/. A +// module sets the hooks it needs; an unset hook means core does what it does +// when the library is absent. +// +// The extension API below is what a module uses. It is generic, keyed by name, +// and names no library. +// +// registerComponentProps(name, fn): JSON props for one component name. +// MountBatchBuilder calls it for that component and stores the result in +// MountOp.extension_props_json. +// registerMountObserver(observer): per-view mount callbacks. The observer owns +// its state; core hands it the view and nothing else. +// registerWindowInsetsObserver(fn): the window's insets, on change. +// registerFrameCallback(fn): called once per frame. A list, not one slot. +// requestFrames(owner, needsFrames): asks core to keep delivering frames while +// that owner needs them. Core stops when no owner asks. A request is not +// "busy": a library may keep a heartbeat running while it has nothing to do. +// reportPendingWork(owner, count): how much work an owner still has queued +// (callbacks waiting for a frame, say). Idle detection waits for every owner +// to report zero; it never waits on frame requests alone. +// registerPointerObserver(fn): raw pointer events before React Native dispatch. +// Each returns the gestures it recognised, which core dispatches to the +// target tag. +// argb(color): a Fabric colour as 0xAARRGGBB, or 0 when unset. Declared here so +// a module's props extraction does not need core's private +// MountBatchBuilder.h. +// turboModules: extra TurboModules, asked in registration order. +#pragma once + +#include "rn-gpui-sys/src/lib.rs.h" // MountOp + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rngpui { + +// Extra TurboModules (the active JS engine's, Rust string modules). Asked in +// registration order, after the app's own provider and before the core +// modules; each returns nullptr when the module is not provided there. +using GpuiTurboModuleFn = + std::function( + const std::string& name, + const std::shared_ptr& jsInvoker)>; + +// Called once per frame with a steady-clock timestamp in ms. +using GpuiFrameFn = std::function; + +namespace extension { + +using ComponentPropsFn = std::function; + +// What an observer sees about a mounted view. Core fills only these fields; +// the observer keeps any state of its own. +struct MountObserver { + std::function + onFrameChanged; + std::function onDeleted; + std::function afterBatch; +}; + +using WindowInsetsFn = + std::function; + +// A raw pointer event, before React Native dispatch. `targetChain` is the hit +// chain from the target outward; `phase` follows RN's pointer phases. +struct RawPointerEvent { + std::vector targetChain; + float x; + float y; + int32_t phase; + double timestamp; +}; + +// A gesture the observer recognised in a pointer event. Core dispatches each +// one back to its target tag. +struct RecognizedGesture { + int32_t targetTag; + std::string eventType; + std::string payloadJson; +}; + +using PointerObserverFn = + std::function(const RawPointerEvent&)>; + +} // namespace extension + +struct GpuiModuleHooks { + std::vector turboModules; + GpuiFrameFn frame; +}; + +// The extension registries. Modules write these; core reads them. +struct GpuiExtensionHooks { + std::unordered_map componentProps; + std::vector mountObservers; + std::vector windowInsetsObservers; + std::vector frameCallbacks; + std::vector pointerObservers; + std::unordered_map frameRequests; +}; + +uint32_t argb(const facebook::react::SharedColor& color); + +// Hooks are set by the installers at startup, before any surface starts. +GpuiModuleHooks& gpuiModuleHooks(); + +// JSON props for one component name. `componentName` is the Fabric component +// name; the ShadowView carries the props and layout. +void registerComponentProps( + const std::string& componentName, + extension::ComponentPropsFn fn); + +// Appends an observer; registration order is call order. +void registerMountObserver(extension::MountObserver observer); + +void registerWindowInsetsObserver(extension::WindowInsetsFn fn); + +void registerFrameCallback(GpuiFrameFn fn); + +// Records whether `owner` still needs frames. Core stops delivering them once +// no owner asks. +void requestFrames(const std::string& owner, bool needsFrames); +// Whether any owner asked for frames right now. +bool anyFrameRequested(); +void reportPendingWork(const std::string& owner, uint64_t count); +uint64_t pendingWork(); + +// Re-evaluates the frame chain from every demand source: native animations, +// and any module that asked through `requestFrames`. Defined in GpuiHost.cpp. +void updateFrameDemand(); + +void registerPointerObserver(extension::PointerObserverFn fn); + +GpuiExtensionHooks& gpuiExtensionHooks(); + +void rn_gpui_install_modules(); + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiMountingManager.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiMountingManager.cpp new file mode 100644 index 0000000000..669be284a4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiMountingManager.cpp @@ -0,0 +1,609 @@ +#include "GpuiMountingManager.h" +#include + +#include +#include +#include +#include +#include "GpuiHost.h" // KAllSurfacesStopped +#include "GpuiHostComponentsCpp.h" +#include "MountBatchBuilder.h" +#include "react/coremodules/DeviceInfoModule.h" +#include "rn-gpui-sys/src/lib.rs.h" // cxx bridge + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace rngpui { +class GpuiModalHostViewComponentDescriptor final + : public ConcreteComponentDescriptor { + public: + using ConcreteComponentDescriptor::ConcreteComponentDescriptor; + + State::Shared createInitialState( + const Props::Shared& /*props*/, + const ShadowNodeFamily::Shared& family) const override { + return std::make_shared( + std::make_shared( + DeviceInfoModule::windowSize()), + family); + } + + protected: + void adopt(ShadowNode& shadowNode) const override { + auto& layoutableShadowNode = + static_cast(shadowNode); + auto& stateData = + static_cast( + *shadowNode.getState()) + .getData(); + layoutableShadowNode.setSize(stateData.screenSize); + layoutableShadowNode.setPositionType(YGPositionTypeAbsolute); + ConcreteComponentDescriptor::adopt(shadowNode); + } +}; + +void GpuiImageLoader::loadImage( + const std::string& uri, + const IImageLoaderOnLoadCallback&& onLoad) { + const auto requestId = nextRequestId_.fetch_add(1); + { + std::lock_guard lock(mutex_); + callbacks_.emplace(requestId, std::move(onLoad)); + } + rn_gpui_load_image(uri, requestId); +} + +IImageLoader::CacheStatus GpuiImageLoader::getCacheStatus( + const std::string& uri) { + return rn_gpui_image_is_cached(uri) ? CacheStatus::Memory : CacheStatus::None; +} + +void GpuiImageLoader::completeRequest( + std::uint64_t requestId, + double width, + double height, + const std::string& error) { + IImageLoaderOnLoadCallback callback; + { + std::lock_guard lock(mutex_); + auto found = callbacks_.find(requestId); + if (found == callbacks_.end()) + return; + callback = std::move(found->second); + callbacks_.erase(found); + } + callback(width, height, error.empty() ? nullptr : error.c_str()); +} + +void rn_gpui_on_image_load_result( + std::uint64_t requestId, + double width, + double height, + rust::String error) { + GpuiImageLoader::completeRequest( + requestId, width, height, std::string(error.data(), error.size())); +} + +namespace { +std::atomic mountBatchesInFlight{0}; + +const char* kindName(MountKind kind) { + switch (kind) { + case MountKind::Create: + return "Create"; + case MountKind::Delete: + return "Delete"; + case MountKind::Insert: + return "Insert"; + case MountKind::Remove: + return "Remove"; + case MountKind::Update: + return "Update"; + } + return "?"; +} + +std::string describeOp(const MountOp& op) { + std::string out = std::string(" ") + kindName(op.kind) + + " tag=" + std::to_string(op.tag) + + " parent=" + std::to_string(op.parent_tag) + + " index=" + std::to_string(op.index) + " " + std::string(op.component); + if (!op.frame.w && !op.frame.h && !op.frame.x && !op.frame.y) { + return out; + } + out += " frame=(" + std::to_string(op.frame.x) + "," + + std::to_string(op.frame.y) + " " + std::to_string(op.frame.w) + "x" + + std::to_string(op.frame.h) + ")"; + if (op.has_paragraph) { + out += " fragments=" + std::to_string(op.paragraph.fragments.size()); + } + if (!op.extension_props_json.empty()) { + out += " props=" + std::string(op.extension_props_json); + } + return out; +} +} // namespace + +void GpuiMountingManager::executeMount( + SurfaceId surfaceId, + MountingTransaction&& transaction) { + MountBatch batch; + batch.surface_id = surfaceId; + if (contentSize_) { + if (auto size = contentSize_(surfaceId)) { + if (size->width > 0.f || size->height > 0.f) { + batch.content_width = size->width; + batch.content_height = size->height; + batch.has_content_size = true; + } + } + } + { + std::lock_guard lock(emittersMutex_); + for (const auto& mutation : transaction.getMutations()) { + batch.ops.push_back(buildMountOp(mutation)); + if (mutation.type == ShadowViewMutation::Insert || + (mutation.type == ShadowViewMutation::Update && + mutation.oldChildShadowView.layoutMetrics.frame != + mutation.newChildShadowView.layoutMetrics.frame)) { + for (const auto& observer : gpuiExtensionHooks().mountObservers) { + if (observer.onFrameChanged) + observer.onFrameChanged(surfaceId, mutation.newChildShadowView); + } + } + auto isVisible = [](const ShadowView& shadowView) { + auto props = std::dynamic_pointer_cast( + shadowView.props); + return props && props->visible; + }; + if (mutation.type == ShadowViewMutation::Insert && + std::string_view(mutation.newChildShadowView.componentName) == + "ModalHostView" && + isVisible(mutation.newChildShadowView)) { + auto emitter = + std::dynamic_pointer_cast( + mutation.newChildShadowView.eventEmitter); + if (emitter) + emitter->onShow({}); + } else if ( + mutation.type == ShadowViewMutation::Update && + std::string_view(mutation.oldChildShadowView.componentName) == + "ModalHostView" && + isVisible(mutation.oldChildShadowView) != + isVisible(mutation.newChildShadowView)) { + auto emitter = + std::dynamic_pointer_cast( + mutation.newChildShadowView.eventEmitter); + if (emitter) { + if (isVisible(mutation.newChildShadowView)) + emitter->onShow({}); + else + emitter->onDismiss({}); + } + } else if ( + mutation.type == ShadowViewMutation::Delete && + std::string_view(mutation.oldChildShadowView.componentName) == + "ModalHostView" && + isVisible(mutation.oldChildShadowView)) { + auto emitter = + std::dynamic_pointer_cast( + mutation.oldChildShadowView.eventEmitter); + if (emitter) + emitter->onDismiss({}); + } + if (mutation.type == ShadowViewMutation::Create || + mutation.type == ShadowViewMutation::Update) { + animatedViews_[mutation.newChildShadowView.tag] = + mutation.newChildShadowView; + emitters_[mutation.newChildShadowView.tag] = + mutation.newChildShadowView.eventEmitter; + const auto componentName = + std::string(mutation.newChildShadowView.componentName); + if (componentName == "ScrollView") { + auto state = + std::dynamic_pointer_cast>( + mutation.newChildShadowView.state); + if (state) + scrollStates_[mutation.newChildShadowView.tag] = std::move(state); + } else if ( + rngpui::anyHostComponents() && + rngpui::hostComponentEntry( + mutation.newChildShadowView.componentHandle) != nullptr) { + auto state = + std::dynamic_pointer_cast>( + mutation.newChildShadowView.state); + if (state) { + hostStates_[mutation.newChildShadowView.tag] = std::move(state); + } + } else if (componentName == "ModalHostView") { + auto state = std::dynamic_pointer_cast< + const ConcreteState>( + mutation.newChildShadowView.state); + if (state) + modalStates_[mutation.newChildShadowView.tag] = std::move(state); + } + } else if (mutation.type == ShadowViewMutation::Delete) { + for (const auto& observer : gpuiExtensionHooks().mountObservers) { + if (observer.onDeleted) + observer.onDeleted(mutation.oldChildShadowView.tag); + } + animatedViews_.erase(mutation.oldChildShadowView.tag); + const auto tag = mutation.oldChildShadowView.tag; + emitters_.erase(tag); + scrollStates_.erase(mutation.oldChildShadowView.tag); + modalStates_.erase(mutation.oldChildShadowView.tag); + hostStates_.erase(mutation.oldChildShadowView.tag); + } + } + } + + rn_gpui_log( + 0, + "executeMount surface=" + std::to_string(surfaceId) + + " ops=" + std::to_string(batch.ops.size())); + for (const auto& op : batch.ops) { + rn_gpui_log(0, describeOp(op)); + } + + mountBatchesInFlight.fetch_add(1, std::memory_order_relaxed); + rn_gpui_on_mount(std::move(batch)); + for (const auto& observer : gpuiExtensionHooks().mountObservers) { + if (observer.afterBatch) + observer.afterBatch(); + } + + if (onAfterMount_) { + onAfterMount_(surfaceId); + } +} + +extern "C" uint64_t rn_gpui_mount_batches_in_flight() noexcept { + return mountBatchesInFlight.load(std::memory_order_relaxed); +} + +extern "C" void rn_gpui_mount_batch_applied() noexcept { + mountBatchesInFlight.fetch_sub(1, std::memory_order_relaxed); +} + +void GpuiMountingManager::synchronouslyUpdateViewOnUIThread( + Tag tag, + const folly::dynamic& props) { + if (gpuiDebugAnimationEnabled()) { + static std::atomic calls{0}; + const auto call = ++calls; + if (call <= 5 || call % 60 == 0) { + LOG(INFO) << "[animated-props] sync tag=" << tag << " call=" << call + << " props=" << folly::toJson(props); + } + } + AnimatedPropsFlat update{}; + update.tag = tag; + if (props.isObject()) { + const auto* opacity = props.get_ptr("opacity"); + if (opacity != nullptr && opacity->isNumber()) { + update.opacity = static_cast(opacity->asDouble()); + update.has_opacity = true; + } + const auto* backgroundColor = props.get_ptr("backgroundColor"); + if (backgroundColor != nullptr && backgroundColor->isNumber()) { + update.background_color = + static_cast(backgroundColor->asDouble()); + update.has_background_color = true; + } + const auto* textColor = props.get_ptr("color"); + if (textColor != nullptr && textColor->isNumber()) { + update.text_color = static_cast(textColor->asDouble()); + update.has_text_color = true; + } + const auto* transform = props.get_ptr("transform"); + if (transform != nullptr && transform->isArray()) { + // Use RN's parser and matrix composition, including operation order, + // layout-dependent translations and transformOrigin. Extracting scalar + // operations independently loses repeated operations and scale/translate + // order. + ShadowView view; + { + std::lock_guard lock(emittersMutex_); + auto it = animatedViews_.find(tag); + if (it == animatedViews_.end()) + return; + view = it->second; + } + auto baseProps = + std::dynamic_pointer_cast(view.props); + if (!baseProps) + return; + ContextContainer contextContainer; + PropsParserContext context(view.surfaceId, contextContainer); + Transform parsed; + fromRawValue(context, RawValue(*transform), parsed); + update.transform = + BaseViewProps::resolveTransform( + view.layoutMetrics.frame.size, parsed, baseProps->transformOrigin) + .matrix; + update.has_transform = true; + } else if (props.empty()) { + update.reset_transform = true; + } + } + if (update.has_opacity || update.has_transform || update.reset_transform || + update.has_background_color || update.has_text_color) { + if (gpuiDebugAnimationEnabled()) { + static std::atomic updates{0}; + const auto updateNumber = ++updates; + if (updateNumber % 60 == 0) { + LOG(INFO) + << "[native-animated] applied animated props through Fabric: tag=" + << tag << " count=" << updateNumber << " opacity=" << update.opacity + << " transform=" << update.has_transform << " scale=(" + << update.transform[0] << ',' << update.transform[5] + << ") translate=(" << update.transform[12] << ',' + << update.transform[13] << ')'; + } + } + rn_gpui_on_animated_props(std::move(update)); + } +} + +namespace { +/// The manager host component reports go to. Set once, when the host makes its +/// manager; the two report functions below read it on the main thread. +std::weak_ptr& hostComponentStateManager() { + static std::weak_ptr manager; + return manager; +} +} // namespace + +void setHostComponentStateManager(std::weak_ptr manager) { + hostComponentStateManager() = std::move(manager); +} + +void reportHostComponentSize(int32_t tag, float width, float height) { + if (auto manager = hostComponentStateManager().lock()) { + manager->setHostComponentSize(tag, width, height); + } +} + +void reportHostComponentInsets( + int32_t tag, + float top, + float right, + float bottom, + float left) { + if (auto manager = hostComponentStateManager().lock()) { + manager->setHostComponentInsets(tag, top, right, bottom, left); + } +} + +void GpuiMountingManager::setHostComponentSize( + Tag tag, + float width, + float height) { + std::shared_ptr> state; + { + std::lock_guard lock(emittersMutex_); + auto it = hostStates_.find(tag); + if (it == hostStates_.end()) { + return; + } + state = it->second; + } + const auto& data = state->getData(); + if (data.measured && data.width == width && data.height == height) { + return; + } + GpuiHostStateData next = data; + next.width = width; + next.height = height; + next.measured = true; + state->updateState(std::move(next)); +} + +void GpuiMountingManager::setHostComponentInsets( + Tag tag, + float top, + float right, + float bottom, + float left) { + std::shared_ptr> state; + { + std::lock_guard lock(emittersMutex_); + auto it = hostStates_.find(tag); + if (it == hostStates_.end()) { + return; + } + state = it->second; + } + const auto& data = state->getData(); + if (data.insetTop == top && data.insetRight == right && + data.insetBottom == bottom && data.insetLeft == left) { + return; + } + GpuiHostStateData next = data; + next.insetTop = top; + next.insetRight = right; + next.insetBottom = bottom; + next.insetLeft = left; + state->updateState(std::move(next)); +} + +void GpuiMountingManager::setScrollOffset(Tag tag, float x, float y) { + std::shared_ptr> state; + { + std::lock_guard lock(emittersMutex_); + auto it = scrollStates_.find(tag); + if (it == scrollStates_.end()) + return; + state = it->second; + } + const auto& data = state->getData(); + state->updateState(ScrollViewState( + {x, y}, data.contentBoundingRect, data.scrollAwayPaddingTop)); +} + +void GpuiMountingManager::setWindowSize(float width, float height) { + std::vector>> states; + { + std::lock_guard lock(emittersMutex_); + states.reserve(modalStates_.size()); + for (const auto& [_, state] : modalStates_) { + states.push_back(state); + } + } + for (const auto& state : states) { + state->updateState(ModalHostViewState(Size{width, height})); + } +} + +void GpuiMountingManager::setWindowTopInset(float top) { + for (const auto& observer : gpuiExtensionHooks().windowInsetsObservers) { + observer(top, 0.0f, 0.0f, 0.0f); + } +} + +void GpuiMountingManager::dispatchEvent( + Tag tag, + const std::string& type, + folly::dynamic&& payload) { + SharedEventEmitter emitter; + { + std::lock_guard lock(emittersMutex_); + auto it = emitters_.find(tag); + if (it != emitters_.end()) { + emitter = it->second; + } else { + // A JS-driven pop has already unmounted its Fabric listener. Keep the + // outgoing pixels for animation, but never enqueue events for a dead + // EventTarget. Native dismissals announce their lifecycle before unmount. + return; + } + } + if (!emitter) { + return; + } + if (type == "topScroll") { + // Only the latest scroll position matters: a queued scroll event that JS + // has not handled yet is replaced rather than piled up behind. + emitter->dispatchUniqueEvent(type, std::move(payload)); + return; + } + emitter->dispatchEvent(type, std::move(payload)); +} + +void GpuiMountingManager::setAccessibilityFocusedView(Tag viewTag) { + rn_gpui_request_accessibility_focus(viewTag); +} + +void GpuiMountingManager::accessibleClickAction(Tag viewTag) { + dispatchEvent(viewTag, "click", folly::dynamic::object("target", viewTag)); +} + +void GpuiMountingManager::dispatchCommand( + const ShadowView& view, + const std::string& name, + const folly::dynamic& args) { + if (std::string_view(view.componentName) == "TextInput") { + if (name == "focus") { + rn_gpui_on_text_input_command(view.tag, true); + } else if (name == "blur") { + rn_gpui_on_text_input_command(view.tag, false); + } else if (name == "clear") { + rn_gpui_on_text_input_clear(view.tag); + } + return; + } + if (!rngpui::anyHostComponents()) { + return; + } + const auto* entry = rngpui::hostComponentEntry(view.componentHandle); + if (entry == nullptr || entry->command == nullptr) { + return; + } + const folly::dynamic normalized = + args.isArray() ? args : folly::dynamic::array(args); + if (!entry->command(view.tag, name, normalized)) { + LOG(ERROR) << "[host-component] " << entry->name << " has no command '" + << name << "'"; + } +} + +void GpuiMountingManager::setSchedulerTaskExecutor( + SchedulerTaskExecutor&& executor) noexcept { + // ReactHost calls this with a real executor when an instance is up + // (`ReactHost.cpp` createReactInstance) and with `nullptr` when it tears the + // instance down (destroyReactInstance, which every reload runs through + // `reloadReactInstance`). Teardown is the only signal the C++ platform gets: + // `SurfaceManager::stopSurface` → `Scheduler::unregisterSurface` emits no + // Delete mutations, so the Rust store is told to drop the whole tree itself. + if (!executor) { + { + std::lock_guard lock(emittersMutex_); + animatedViews_.clear(); + modalStates_.clear(); + } + rn_gpui_on_surface_stopped(kAllSurfacesStopped); + } +} + +ComponentRegistryFactory GpuiMountingManager::getComponentRegistryFactory() { + return [](const EventDispatcher::Weak& eventDispatcher, + const std::shared_ptr& contextContainer) { + static auto registry = [] { + auto result = std::make_shared(); + result->add( + concreteComponentDescriptorProvider()); + result->add( + concreteComponentDescriptorProvider()); + result->add( + concreteComponentDescriptorProvider()); + result->add( + concreteComponentDescriptorProvider()); + result->add( + concreteComponentDescriptorProvider()); + result->add( + concreteComponentDescriptorProvider()); + result->add( + concreteComponentDescriptorProvider()); + result->add(concreteComponentDescriptorProvider< + GpuiModalHostViewComponentDescriptor>()); + result->add(concreteComponentDescriptorProvider< + NativeViewHostComponentDescriptor>()); + result->add(concreteComponentDescriptorProvider< + WindowControlComponentDescriptor>()); + result->add(concreteComponentDescriptorProvider< + DesktopContextMenuViewComponentDescriptor>()); + result->add(concreteComponentDescriptorProvider< + ShortcutScopeComponentDescriptor>()); + result->setComponentDescriptorProviderRequest([](ComponentName name) { + LOG(ERROR) << "Fabric component " << name + << " is not implemented on gpui"; + }); + rn_gpui_register_app_components(*result); + registerHostComponentDescriptors(*result); + return result; + }(); + return registry->createComponentDescriptorRegistry( + {eventDispatcher, contextContainer, nullptr}); + }; +} +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiMountingManager.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiMountingManager.h new file mode 100644 index 0000000000..d4d72ec1db --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiMountingManager.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "GpuiHostComponentsCpp.h" +#include "GpuiImageLoader.h" +#include "GpuiModules.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace rngpui { +extern "C" uint64_t rn_gpui_mount_batches_in_flight() noexcept; +extern "C" void rn_gpui_mount_batch_applied() noexcept; + +void rn_gpui_register_app_components( + facebook::react::ComponentDescriptorProviderRegistry& registry); + +using namespace facebook::react; + +class GpuiImageLoader final : public IImageLoader { + public: + void loadImage( + const std::string& uri, + const IImageLoaderOnLoadCallback&& onLoad) override; + CacheStatus getCacheStatus(const std::string& uri) override; + static void + completeRequest(std::uint64_t, double, double, const std::string&); + + private: + inline static std::atomic nextRequestId_{1}; + inline static std::mutex mutex_; + inline static std::unordered_map + callbacks_; +}; + +class GpuiMountingManager : public IMountingManager { + public: + /// `contentSize` returns the size a surface's root was laid out at, or + /// nothing when the surface is not running. It is asked once per mount batch, + /// which is what a `FitContent` surface reports back to GPUI. + explicit GpuiMountingManager( + std::function&& onAfterMount, + std::function(SurfaceId)>&& + contentSize = nullptr) + : onAfterMount_(std::move(onAfterMount)), + contentSize_(std::move(contentSize)) {} + + void executeMount(SurfaceId surfaceId, MountingTransaction&& tx) override; + void synchronouslyUpdateViewOnUIThread(Tag tag, const folly::dynamic& props) + override; + void dispatchCommand( + const ShadowView& view, + const std::string& name, + const folly::dynamic& args) override; + ComponentRegistryFactory getComponentRegistryFactory() override; + std::shared_ptr getImageLoader() noexcept override { + return imageLoader_; + } + + // ReactHost calls this with a real executor once a fresh instance + // is up (`ReactHost.cpp`, createReactInstance) and with `nullptr` when it + // tears the instance down (destroyReactInstance, i.e. every reload). The + // teardown call is the only surface-lifecycle signal the C++ platform gets: + // stopping a surface emits no Delete mutations (`SurfaceManager::stopSurface` + // → `Scheduler::unregisterSurface`), so it is where the Rust store is told to + // drop the whole tree before the reloaded instance mounts over it. + void setSchedulerTaskExecutor( + SchedulerTaskExecutor&& executor) noexcept override; + + // Called from the GPUI main thread via GpuiHost::dispatch_event. + void + dispatchEvent(Tag tag, const std::string& type, folly::dynamic&& payload); + void setScrollOffset(Tag tag, float x, float y); + + /// Pushes a size a host component reported into its shadow node's state, + /// which is what `measureContent` reads, and asks for a commit so Yoga + /// re-measures. A size equal to the state's current one is dropped here, by + /// comparing against the state data rather than a second copy of it. + void setHostComponentSize(Tag tag, float width, float height); + + /// Pushes a container's reported insets into its shadow node's state, where + /// `adopt` applies them as Yoga padding. Insets equal to the state's current + /// ones are dropped here, for the same reason. + void setHostComponentInsets( + Tag tag, + float top, + float right, + float bottom, + float left); + void setWindowSize(float width, float height); + void setWindowTopInset(float top); + void setAccessibilityFocusedView(Tag viewTag) override; + void accessibleClickAction(Tag viewTag) override; + + private: + std::function onAfterMount_; + std::function(SurfaceId)> contentSize_; + std::shared_ptr imageLoader_ = + std::make_shared(); + std::mutex emittersMutex_; + std::unordered_map animatedViews_; + std::unordered_map + emitters_; // written on JS thread, read on main + std::unordered_map>> + scrollStates_; + std::unordered_map< + Tag, + std::shared_ptr>> + modalStates_; + std::unordered_map< + Tag, + std::shared_ptr>> + hostStates_; +}; +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiNodeAddons.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiNodeAddons.cpp new file mode 100644 index 0000000000..bad8f67266 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiNodeAddons.cpp @@ -0,0 +1,442 @@ +// Core's half of stock Node-API addon loading: the `_requireNodeLibrary` JSI +// global, path resolution, the per-runtime cache and the error messages. +// +// The engine supplies the VM-specific half — creating a napi_env and dlopening +// the file — through the GpuiJsEngine::NodeAddons capability. Nothing here +// includes a Hermes header. +#include "GpuiNodeAddons.h" +#include "GpuiJsEngine.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#elif defined(__APPLE__) +#include +#include +#elif defined(__linux__) +#include +#endif + +namespace rngpui { +namespace { + +constexpr const char* kAddonCacheProperty = "__rnGpuiNodeAddonCache"; + +/// The platform's list separator for RN_GPUI_NODE_ADDON_PATH. +#if defined(_WIN32) +constexpr char kPathListSeparator = ';'; +#else +constexpr char kPathListSeparator = ':'; +#endif + +/// The UTF-8 value of `RN_GPUI_NODE_ADDON_PATH`, or empty when unset. +/// +/// On Windows the variable is read as wide characters and converted, rather +/// than with `std::getenv`: `getenv` returns the bytes the process's ANSI code +/// page produces, which cannot represent a directory name outside that page — +/// the same problem `pathFromUtf8` solves for the strings that come from +/// JavaScript. POSIX uses `getenv` unchanged, where the bytes are the path. +std::string nodeAddonPathEnvironment() { +#if defined(_WIN32) + const wchar_t* wide = _wgetenv(L"RN_GPUI_NODE_ADDON_PATH"); + if (wide == nullptr || *wide == L'\0') + return {}; + const int length = + WideCharToMultiByte(CP_UTF8, 0, wide, -1, nullptr, 0, nullptr, nullptr); + if (length <= 0) + return {}; + // `length` includes the terminator when it is asked for with -1. + std::string utf8(static_cast(length - 1), '\0'); + WideCharToMultiByte( + CP_UTF8, 0, wide, -1, utf8.data(), length, nullptr, nullptr); + return utf8; +#else + const char* value = std::getenv("RN_GPUI_NODE_ADDON_PATH"); + return value == nullptr ? std::string() : std::string(value); +#endif +} + +/// Splits `value` on every character in `separators`, dropping empty entries. +std::vector splitList(const char* value, const char* separators) { + std::vector parts; + if (value == nullptr) + return parts; + std::string current; + for (const char* cursor = value; *cursor != '\0'; ++cursor) { + if (std::strchr(separators, *cursor) != nullptr) { + if (!current.empty()) + parts.push_back(current); + current.clear(); + } else { + current.push_back(*cursor); + } + } + if (!current.empty()) + parts.push_back(current); + return parts; +} + +/// The directory holding the running executable, or an empty path. A packaged +/// app keeps its addons beside the executable, which is what makes this +/// meaningful; a dev build falls through to the embedded absolute path. +std::filesystem::path executableDirectory() { +#if defined(_WIN32) + std::wstring buffer(MAX_PATH, L'\0'); + for (;;) { + const DWORD length = GetModuleFileNameW( + nullptr, buffer.data(), static_cast(buffer.size())); + if (length == 0) + return {}; + if (length < buffer.size()) { + buffer.resize(length); + return std::filesystem::path(buffer).parent_path(); + } + // Truncated: the path is longer than the buffer, so grow and retry. + buffer.resize(buffer.size() * 2); + } +#elif defined(__APPLE__) + uint32_t size = 0; + _NSGetExecutablePath(nullptr, &size); + if (size == 0) + return {}; + std::vector buffer(size); + if (_NSGetExecutablePath(buffer.data(), &size) != 0) + return {}; + std::error_code error; + // The path from _NSGetExecutablePath may be a symlink or contain `..`, so + // resolve it the same way /proc/self/exe is already resolved on Linux. + auto resolved = std::filesystem::canonical(buffer.data(), error); + if (error) + return std::filesystem::path(buffer.data()).parent_path(); + return resolved.parent_path(); +#elif defined(__linux__) + std::error_code error; + auto path = std::filesystem::read_symlink("/proc/self/exe", error); + if (error) + return {}; + return path.parent_path(); +#else + return {}; +#endif +} + +bool isRegularFile(const std::filesystem::path& path) { + std::error_code error; + const bool result = std::filesystem::is_regular_file(path, error); + return result && !error; +} + +/// A filesystem path as UTF-8. +/// +/// `path::string()` is wrong for this on Windows: it converts the native wide +/// form with the process's ANSI code page, so a path holding a character +/// outside that page comes back with `?` in it and the addon is not found (or, +/// worse, the wrong file is). Every consumer here — the JS error message, the +/// addon cache key, and the engine's `load_module` — speaks UTF-8, so convert +/// to that explicitly. +std::string pathToUtf8(const std::filesystem::path& path) { +#if defined(_WIN32) + const std::wstring wide = path.native(); + if (wide.empty()) + return {}; + const int length = WideCharToMultiByte( + CP_UTF8, + 0, + wide.data(), + static_cast(wide.size()), + nullptr, + 0, + nullptr, + nullptr); + if (length <= 0) + return {}; + std::string utf8(static_cast(length), '\0'); + WideCharToMultiByte( + CP_UTF8, + 0, + wide.data(), + static_cast(wide.size()), + utf8.data(), + length, + nullptr, + nullptr); + return utf8; +#else + // POSIX paths are bytes, and callers hand us UTF-8, so this is the identity. + return path.string(); +#endif +} + +/// The inverse of `pathToUtf8`: a `std::filesystem::path` for a path that came +/// from JavaScript, which is UTF-8. +/// +/// On Windows `std::filesystem::path(std::string)` decodes the bytes with the +/// process's ANSI code page, so a checkout under `C:\Users\José\` or any addon +/// directory with an accented character in its name produced a path that does +/// not name the file — the addon was then reported as missing, with the mangled +/// path in the message. On POSIX the bytes are the path, so this is the +/// identity, exactly as `pathToUtf8` is. +std::filesystem::path pathFromUtf8(const std::string& utf8) { +#if defined(_WIN32) + if (utf8.empty()) + return {}; + const int length = MultiByteToWideChar( + CP_UTF8, + MB_ERR_INVALID_CHARS, + utf8.data(), + static_cast(utf8.size()), + nullptr, + 0); + if (length <= 0) { + // Not valid UTF-8. Fall back to the narrow constructor rather than + // returning nothing: a caller that passed a byte string still gets the old + // behaviour instead of a silent miss. + return std::filesystem::path(utf8); + } + std::wstring wide(static_cast(length), L'\0'); + MultiByteToWideChar( + CP_UTF8, + MB_ERR_INVALID_CHARS, + utf8.data(), + static_cast(utf8.size()), + wide.data(), + length); + return std::filesystem::path(wide); +#else + return std::filesystem::path(utf8); +#endif +} + +/// First existing regular file wins, in the order section 3.4 of the plan +/// spells out. `tried` collects every location in the same order so the error +/// message can list them. +std::filesystem::path resolveAddonPath( + const std::string& key, + const std::optional& embeddedPath, + std::vector& tried) { + std::vector candidates; + // Every string here comes from JavaScript or from the environment, so every + // one of them is decoded from UTF-8. `key` is a relative path with `/` + // separators; joining it as a path keeps that meaning. + const auto keyPath = pathFromUtf8(key); + const std::string addonPath = nodeAddonPathEnvironment(); + for (const auto& directory : splitList( + addonPath.c_str(), std::string(1, kPathListSeparator).c_str())) { + candidates.emplace_back(pathFromUtf8(directory) / keyPath); + } + const auto exeDir = executableDirectory(); + if (!exeDir.empty()) { + candidates.push_back(exeDir / "node-addons" / keyPath); +#if defined(__APPLE__) + // A macOS .app bundle keeps resources beside the executable, inside + // Contents/Resources rather than next to the Mach-O in Contents/MacOS. + candidates.push_back(exeDir / ".." / "Resources" / "node-addons" / keyPath); +#endif + } + if (embeddedPath.has_value()) { + candidates.emplace_back(pathFromUtf8(*embeddedPath)); + } + for (const auto& candidate : candidates) { + tried.push_back(pathToUtf8(candidate)); + if (isRegularFile(candidate)) { + std::error_code error; + // The cache is keyed by the real path, so two keys that resolve to one + // file (a symlinked package, say) share one load. + auto canonical = std::filesystem::canonical(candidate, error); + return error ? candidate : canonical; + } + } + return {}; +} + +std::string missingAddonMessage( + const std::string& key, + const std::vector& tried) { + std::string message = "Cannot find Node-API addon '" + key + "'. Looked in:"; + for (const auto& location : tried) { + message += "\n " + location; + } + return message; +} + +/// Where a failed call leaves its message and code for the JS wrapper to throw. +constexpr const char* kAddonFailureProperty = "__rnGpuiNodeAddonFailure"; + +/// Records a failure for `_requireNodeLibrary` to turn into a thrown `Error`. +/// +/// The error deliberately is *not* thrown from here. A C++ `jsi::JSError` +/// thrown out of a host function does not reach JS intact: the runtime fails to +/// match it as a `JSError` and re-wraps it through its generic `std::exception` +/// path, which prefixes the message with "Exception in HostFunction" and +/// discards every property except the message string — `code` included. Leaving +/// the message and code on a global, and letting the JavaScript wrapper below +/// throw, keeps both and drops the prefix. +void setFailure( + facebook::jsi::Runtime& rt, + const std::string& message, + const char* code) { + auto failure = facebook::jsi::Object(rt); + failure.setProperty( + rt, "message", facebook::jsi::String::createFromUtf8(rt, message)); + if (code != nullptr) { + failure.setProperty( + rt, "code", facebook::jsi::String::createFromUtf8(rt, code)); + } + rt.global().setProperty(rt, kAddonFailureProperty, std::move(failure)); +} + +/// The cache of already-loaded exports, stored on the runtime's global object +/// so it lives exactly as long as the runtime — a reload gets a fresh one. +facebook::jsi::Object addonCache(facebook::jsi::Runtime& rt) { + auto global = rt.global(); + auto existing = global.getProperty(rt, kAddonCacheProperty); + if (existing.isObject()) { + return existing.asObject(rt); + } + auto cache = facebook::jsi::Object(rt); + global.setProperty(rt, kAddonCacheProperty, cache); + return cache; +} + +} // namespace + +void installNodeAddons( + facebook::jsi::Runtime& rt, + std::shared_ptr jsInvoker) { + const auto& engine = activeJsEngine(); + + // The loader is built once per runtime, from the engine's capability. When + // the engine has none, the global still exists and fails with a message + // naming the engine, so app code does not have to feature-detect. + std::shared_ptr> + loader; + if (engine.nodeAddons) { + loader = std::make_shared>( + engine.nodeAddons->makeLoader(std::move(jsInvoker))); + } + const std::string unsupportedMessage = + "Node addons are not supported by the " + engine.info.name + " JS engine"; + + auto requireNodeLibrary = facebook::jsi::Function::createFromHostFunction( + rt, + facebook::jsi::PropNameID::forAscii(rt, "_requireNodeLibraryNative"), + 2, + [loader, unsupportedMessage]( + facebook::jsi::Runtime& rt, + const facebook::jsi::Value&, + const facebook::jsi::Value* args, + size_t count) -> facebook::jsi::Value { + if (count < 1 || !args[0].isString()) { + setFailure( + rt, + "_requireNodeLibrary expects a key string as its first " + "argument", + "ERR_INVALID_ARG_TYPE"); + return facebook::jsi::Value::undefined(); + } + const std::string key = args[0].asString(rt).utf8(rt); + + std::optional embeddedPath; + if (count >= 2 && !args[1].isUndefined() && !args[1].isNull()) { + if (!args[1].isString()) { + setFailure( + rt, + "_requireNodeLibrary expects its second argument, when " + "present, to be a path string", + "ERR_INVALID_ARG_TYPE"); + return facebook::jsi::Value::undefined(); + } + embeddedPath = args[1].asString(rt).utf8(rt); + } + + std::vector tried; + const auto resolved = resolveAddonPath(key, embeddedPath, tried); + if (resolved.empty()) { + setFailure(rt, missingAddonMessage(key, tried), "MODULE_NOT_FOUND"); + return facebook::jsi::Value::undefined(); + } + const std::string resolvedPath = pathToUtf8(resolved); + + auto cache = addonCache(rt); + // Prefixed so a path can never collide with an Object.prototype name + // such as "__proto__". + const std::string cacheKey = "addon:" + resolvedPath; + auto cached = cache.getProperty(rt, cacheKey.c_str()); + if (!cached.isUndefined()) { + return cached; + } + + if (!loader) { + setFailure(rt, unsupportedMessage, "ERR_NODE_ADDONS_UNSUPPORTED"); + return facebook::jsi::Value::undefined(); + } + bool loadFailed = false; + auto exports = facebook::jsi::Value::undefined(); + try { + exports = (*loader)(rt, resolvedPath); + } catch (const facebook::jsi::JSError& error) { + // A failed load is reported the same way as a failed resolution: the + // engine's message (which carries `dlerror()`'s text, naming the + // missing symbol) is left for the JS wrapper to throw. Letting this + // exception cross the host-function boundary instead would prefix it + // with "Exception in HostFunction". + loadFailed = true; + setFailure(rt, error.getMessage(), "ERR_DLOPEN_FAILED"); + } + if (loadFailed) { + return facebook::jsi::Value::undefined(); + } + cache.setProperty(rt, cacheKey.c_str(), exports); + return exports; + }); + + // The JS-visible loader. It calls the native function and throws a genuine + // JavaScript `Error` when that recorded a failure, so the `code` property and + // the multi-line message reach the caller unchanged. See `setFailure`. + const std::string failureProperty = kAddonFailureProperty; + const std::string wrapperSource = + "(function (native) {\n" + " return function _requireNodeLibrary(key, path) {\n" + " const result = native(key, path);\n" + " const failure = globalThis." + + failureProperty + + ";\n" + " if (failure) {\n" + " delete globalThis." + + failureProperty + + ";\n" + " const error = new Error(failure.message);\n" + " if (failure.code) error.code = failure.code;\n" + " throw error;\n" + " }\n" + " return result;\n" + " };\n" + "})\n"; + auto wrapper = rt.evaluateJavaScript( + std::make_unique(wrapperSource), + "node-addons.js"); + // `Function` has no call operator; the runtime does the call. + auto requireNodeLibraryWithErrors = + wrapper.getObject(rt).getFunction(rt).call( + rt, std::move(requireNodeLibrary)); + + rt.global().setProperty( + rt, "_requireNodeLibrary", std::move(requireNodeLibraryWithErrors)); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiNodeAddons.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiNodeAddons.h new file mode 100644 index 0000000000..92331b9a90 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiNodeAddons.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +namespace facebook::react { +class CallInvoker; +} // namespace facebook::react + +namespace rngpui { + +/// Installs the `_requireNodeLibrary(key, path)` JSI global on `runtime`. +/// +/// Called once per runtime, on the JS thread, before the app bundle runs — a +/// reload creates a new runtime and reinstalls it, so the addon cache (which +/// lives on that runtime's global object) starts empty, as Node does for a new +/// context. +/// +/// `jsInvoker` schedules work on this runtime's JS thread; it is handed to the +/// engine, which owns the Node-API host callbacks (async work, thread-safe +/// functions). +/// +/// Core owns everything else: resolving `key` to a file, the cache of loaded +/// exports, and the error messages, including the one used when the active +/// engine has no NodeAddons capability. +void installNodeAddons( + facebook::jsi::Runtime& runtime, + std::shared_ptr jsInvoker); + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiRustTurboModules.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiRustTurboModules.cpp new file mode 100644 index 0000000000..7024009fff --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiRustTurboModules.cpp @@ -0,0 +1,662 @@ +#include "GpuiRustTurboModules.h" +#include "GpuiDynamic.h" +#include "GpuiModules.h" +#include "GpuiTurboModule.h" +#include "rn-gpui-sys/src/lib.rs.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rngpui { +using namespace facebook::react; + +namespace { + +namespace jsi = facebook::jsi; + +/// A copy of one node tree, so a value built in a scope can cross to the JS +/// thread after the scope is closed. `folly::dynamic` cannot hold the bytes of +/// an `ArrayBuffer`. +struct NodeValue { + uint8_t kind{DynamicNull}; + bool flag{false}; + double number{0.0}; + std::string text; + std::vector bytes; + std::vector> children; +}; + +NodeValue nodeValueFromScope(uint32_t scope, uint32_t node) { + NodeValue value; + value.kind = dynamic_node_kind(scope, node); + switch (value.kind) { + case DynamicBool: + value.flag = dynamic_node_bool(scope, node); + break; + case DynamicNumber: + value.number = dynamic_node_number(scope, node); + break; + case DynamicString: { + const rust::String text = dynamic_node_text(scope, node); + value.text.assign(text.data(), text.size()); + break; + } + case DynamicBuffer: { + const rust::Vec bytes = dynamic_node_buffer(scope, node); + value.bytes.assign(bytes.data(), bytes.data() + bytes.size()); + break; + } + case DynamicArray: + case DynamicObject: { + const size_t length = dynamic_node_len(scope, node); + value.children.reserve(length); + for (size_t index = 0; index < length; ++index) { + const std::string key = value.kind == DynamicObject + ? std::string(dynamic_node_key(scope, node, index)) + : std::string(); + value.children.emplace_back( + key, + nodeValueFromScope(scope, dynamic_node_child(scope, node, index))); + } + break; + } + default: + break; + } + return value; +} + +jsi::Value jsiValueFromNodeValue(jsi::Runtime& rt, const NodeValue& value) { + switch (value.kind) { + case DynamicBool: + return jsi::Value(value.flag); + case DynamicNumber: + return jsi::Value(value.number); + case DynamicString: + return jsi::String::createFromUtf8(rt, value.text); + case DynamicBuffer: + return { + rt, + rt.createArrayBuffer( + std::make_shared( + value.bytes))}; + case DynamicArray: { + jsi::Array array(rt, value.children.size()); + for (size_t index = 0; index < value.children.size(); ++index) { + array.setValueAtIndex( + rt, index, jsiValueFromNodeValue(rt, value.children[index].second)); + } + return jsi::Value(rt, array); + } + case DynamicObject: { + jsi::Object object(rt); + for (const auto& child : value.children) { + object.setProperty( + rt, child.first.c_str(), jsiValueFromNodeValue(rt, child.second)); + } + return jsi::Value(rt, object); + } + default: + return jsi::Value::null(); + } +} + +/// Writes a JS value into a scope, following arrays and objects so an +/// `ArrayBuffer` nested in either keeps its bytes. +uint32_t +nodeFromJsValue(jsi::Runtime& rt, uint32_t scope, const jsi::Value& value) { + if (value.isNull() || value.isUndefined()) { + return dynamic_node_new(scope, DynamicNull); + } + if (value.isBool()) { + const uint32_t node = dynamic_node_new(scope, DynamicBool); + dynamic_node_set_bool(scope, node, value.getBool()); + return node; + } + if (value.isNumber()) { + const uint32_t node = dynamic_node_new(scope, DynamicNumber); + dynamic_node_set_number(scope, node, value.getNumber()); + return node; + } + if (value.isString()) { + const uint32_t node = dynamic_node_new(scope, DynamicString); + dynamic_node_set_text(scope, node, value.getString(rt).utf8(rt)); + return node; + } + if (value.isObject()) { + const jsi::Object object = value.getObject(rt); + if (object.isArrayBuffer(rt)) { + const jsi::ArrayBuffer buffer = object.getArrayBuffer(rt); + const uint8_t* data = buffer.data(rt); + const uint32_t node = dynamic_node_new(scope, DynamicBuffer); + dynamic_node_set_buffer( + scope, node, rust::Slice(data, buffer.size(rt))); + return node; + } + if (object.isFunction(rt)) { + // Only a parameter the spec declares as a callback is a JS function. + return dynamic_node_new(scope, DynamicNull); + } + if (object.isArray(rt)) { + const jsi::Array array = object.getArray(rt); + const uint32_t node = dynamic_node_new(scope, DynamicArray); + for (size_t index = 0; index < array.size(rt); ++index) { + dynamic_node_append( + scope, + node, + nodeFromJsValue(rt, scope, array.getValueAtIndex(rt, index)), + rust::Str()); + } + return node; + } + const uint32_t node = dynamic_node_new(scope, DynamicObject); + const jsi::Array names = object.getPropertyNames(rt); + for (size_t index = 0; index < names.size(rt); ++index) { + const std::string key = + names.getValueAtIndex(rt, index).asString(rt).utf8(rt); + dynamic_node_append( + scope, + node, + nodeFromJsValue(rt, scope, object.getProperty(rt, key.c_str())), + key); + } + return node; + } + return dynamic_node_new(scope, DynamicNull); +} + +/// A Rust promise result. A `Promise` resolves with `undefined`, which a +/// node cannot express on its own. +struct RustPromiseResult { + std::optional value; +}; + +struct PendingCall { + rngpui::Promise promise; + bool returnsVoid{false}; +}; + +std::mutex& pendingMutex() { + static std::mutex mutex; + return mutex; +} + +std::unordered_map& pendingCalls() { + static std::unordered_map calls; + return calls; +} + +std::atomic& nextRequestId() { + static std::atomic next{1}; + return next; +} + +std::mutex& callbacksMutex() { + static std::mutex mutex; + return mutex; +} + +std::unordered_map>& callbacks() { + static std::unordered_map> map; + return map; +} + +std::atomic& nextCallbackId() { + static std::atomic next{1}; + return next; +} + +std::mutex& modulesMutex() { + static std::mutex mutex; + return mutex; +} + +std::unordered_map>& modules() { + static std::unordered_map> map; + return map; +} + +/// One generic TurboModule serves every Rust module: the method list comes from +/// the Rust registry when JS first asks for the module. +class RustTurboModule final + : public TurboModule, + public std::enable_shared_from_this { + public: + RustTurboModule( + std::string name, + std::shared_ptr jsInvoker, + uint64_t handle, + rust::Vec methods, + rust::Vec events) + : TurboModule(std::move(name), std::move(jsInvoker)), handle_(handle) { + methods_.reserve(methods.size()); + for (const auto& method : methods) { + Method entry{ + .name = std::string(method.name), + .argCount = method.arg_count, + .requiredArgs = method.required_args, + .kind = method.kind, + .returnsVoid = method.returns_void, + .callbackArgs = std::vector( + method.callback_args.begin(), method.callback_args.end()), + }; + methodIndex_.emplace(entry.name, methods_.size()); + methods_.push_back(std::move(entry)); + } + for (const auto& event : events) { + const std::string name(event.name); + if (event.has_value) { + eventEmitterMap_[name] = + std::make_shared>(); + } else { + eventEmitterMap_[name] = std::make_shared>(); + } + } + } + + /// `weak_from_this` is empty inside the constructor, and events look the + /// module up by handle. + void start() { + std::lock_guard lock(modulesMutex()); + modules()[handle_] = weak_from_this(); + } + + ~RustTurboModule() override { + { + std::lock_guard lock(modulesMutex()); + modules().erase(handle_); + } + rn_gpui_rust_module_drop(handle_); + } + + std::vector getPropertyNames( + jsi::Runtime& runtime) override { + std::vector names; + names.reserve(methods_.size()); + for (const auto& method : methods_) { + names.push_back(jsi::PropNameID::forUtf8(runtime, method.name)); + } + return names; + } + + void emitValueEvent(const std::string& event, NodeValue value) { + auto found = eventEmitterMap_.find(event); + if (found == eventEmitterMap_.end()) { + return; + } + auto emitter = std::dynamic_pointer_cast>( + found->second); + if (!emitter) { + return; + } + emitter->emit([value = std::move(value)](jsi::Runtime& rt) -> jsi::Value { + return jsiValueFromNodeValue(rt, value); + }); + } + + void emitVoidEvent(const std::string& event) { + auto found = eventEmitterMap_.find(event); + if (found == eventEmitterMap_.end()) { + return; + } + auto emitter = + std::dynamic_pointer_cast>(found->second); + if (emitter) { + emitter->emit(); + } + } + + void sendDeviceEvent(const std::string& name, NodeValue body) { + emitDeviceEvent( + name, + [body = std::move(body)]( + jsi::Runtime& rt, std::vector& args) { + args.emplace_back(jsiValueFromNodeValue(rt, body)); + }); + } + + protected: + jsi::Value create(jsi::Runtime& runtime, const jsi::PropNameID& propName) + override { + const std::string name = propName.utf8(runtime); + auto found = methodIndex_.find(name); + if (found == methodIndex_.end()) { + return TurboModule::create(runtime, propName); + } + const size_t index = found->second; + return jsi::Function::createFromHostFunction( + runtime, + propName, + static_cast(methods_[index].argCount), + [this, index]( + jsi::Runtime& rt, + const jsi::Value&, + const jsi::Value* args, + size_t count) { return invoke(rt, index, args, count); }); + } + + private: + struct Method { + std::string name; + uint32_t argCount; + uint32_t requiredArgs; + uint8_t kind; + bool returnsVoid; + std::vector callbackArgs; + }; + + class ScopeGuard { + public: + explicit ScopeGuard(uint32_t scope) : scope_(scope) {} + ~ScopeGuard() { + dynamic_scope_close(scope_); + } + ScopeGuard(const ScopeGuard&) = delete; + ScopeGuard& operator=(const ScopeGuard&) = delete; + + private: + uint32_t scope_; + }; + + void throwError( + jsi::Runtime& rt, + const rust::String& code, + const rust::String& message) const { + auto error = jsi::Value(rt, jsi::JSError(rt, std::string(message)).value()); + if (!code.empty()) { + error.asObject(rt).setProperty( + rt, "code", jsi::String::createFromUtf8(rt, std::string(code))); + } + throw jsi::JSError(rt, std::move(error)); + } + + /// A JS function cannot be a value, so the id of a stored + /// `AsyncCallback` takes its place. + uint64_t storeCallback(jsi::Runtime& rt, const jsi::Value& value) { + auto callback = + AsyncCallback<>(rt, value.getObject(rt).getFunction(rt), jsInvoker_); + const uint64_t id = nextCallbackId().fetch_add(1); + std::lock_guard lock(callbacksMutex()); + callbacks().emplace(id, std::move(callback)); + return id; + } + + jsi::Value + invoke(jsi::Runtime& rt, size_t index, const jsi::Value* args, size_t count) { + const Method& method = methods_[index]; + if (count < method.requiredArgs) { + throw jsi::JSError( + rt, + name_ + "." + method.name + " expects " + + std::to_string(method.requiredArgs) + " arguments, got " + + std::to_string(count)); + } + const uint32_t scope = dynamic_scope_open(); + const ScopeGuard guard(scope); + const uint32_t array = dynamic_node_new(scope, DynamicArray); + for (uint32_t position = 0; position < method.argCount; ++position) { + if (position >= count) { + dynamic_node_append( + scope, array, dynamic_node_new(scope, DynamicNull), rust::Str()); + continue; + } + const bool isCallback = std::find( + method.callbackArgs.begin(), + method.callbackArgs.end(), + position) != method.callbackArgs.end(); + if (isCallback && args[position].isObject() && + args[position].getObject(rt).isFunction(rt)) { + const uint64_t id = storeCallback(rt, args[position]); + const uint32_t node = dynamic_node_new(scope, DynamicNumber); + dynamic_node_set_number(scope, node, static_cast(id)); + dynamic_node_append(scope, array, node, rust::Str()); + continue; + } + dynamic_node_append( + scope, + array, + nodeFromJsValue(rt, scope, args[position]), + rust::Str()); + } + + switch (method.kind) { + case 0: { + bool ok = false; + rust::String code; + rust::String message; + const uint32_t root = rn_gpui_rust_module_call_value( + handle_, + static_cast(index), + scope, + array, + ok, + code, + message); + if (!ok) { + throwError(rt, code, message); + } + return jsiValueFromNodeValue(rt, nodeValueFromScope(scope, root)); + } + case 1: { + bool ok = false; + rust::String code; + rust::String message; + rn_gpui_rust_module_call_void( + handle_, + static_cast(index), + scope, + array, + ok, + code, + message); + if (!ok) { + throwError(rt, code, message); + } + return jsi::Value::undefined(); + } + default: { + rngpui::Promise promise(rt, jsInvoker_); + const int32_t request = nextRequestId().fetch_add(1); + { + std::lock_guard lock(pendingMutex()); + pendingCalls().emplace( + request, PendingCall{promise, method.returnsVoid}); + } + rn_gpui_rust_module_call_promise( + handle_, static_cast(index), scope, array, request); + return promise.get(rt); + } + } + } + + uint64_t handle_; + std::vector methods_; + std::unordered_map methodIndex_; +}; + +std::shared_ptr moduleFor(uint64_t handle) { + std::lock_guard lock(modulesMutex()); + auto found = modules().find(handle); + if (found == modules().end()) { + return nullptr; + } + return found->second.lock(); +} + +std::vector arrayFromNode(uint32_t scope, uint32_t node) { + std::vector values; + const size_t length = dynamic_node_len(scope, node); + values.reserve(length); + for (size_t index = 0; index < length; ++index) { + values.push_back( + nodeValueFromScope(scope, dynamic_node_child(scope, node, index))); + } + return values; +} + +} // namespace + +} // namespace rngpui + +namespace facebook::react { + +template <> +struct Bridging { + static jsi::Value toJs( + jsi::Runtime& rt, + const rngpui::RustPromiseResult& result, + const std::shared_ptr&) { + if (!result.value) { + return jsi::Value::undefined(); + } + return jsiValueFromNodeValue(rt, *result.value); + } +}; + +} // namespace facebook::react + +namespace rngpui { + +void rn_gpui_rust_module_result( + int32_t request, + bool ok, + uint32_t scope, + uint32_t root, + rust::String code, + rust::String message) { + std::optional pending; + { + std::lock_guard lock(pendingMutex()); + auto found = pendingCalls().find(request); + if (found == pendingCalls().end()) { + // The JS promise is gone: the runtime was torn down by a reload. + dynamic_scope_close(scope); + return; + } + pending = std::move(found->second); + pendingCalls().erase(found); + } + if (ok) { + if (pending->returnsVoid || root == kDynamicNoNode) { + pending->promise.resolve(RustPromiseResult{std::nullopt}); + } else { + pending->promise.resolve( + RustPromiseResult{nodeValueFromScope(scope, root)}); + } + } else { + pending->promise.reject(std::string(code), std::string(message)); + } + dynamic_scope_close(scope); +} + +void rn_gpui_rust_callback_invoke(uint64_t id, uint32_t scope, uint32_t root) { + std::optional> callback; + { + std::lock_guard lock(callbacksMutex()); + auto found = callbacks().find(id); + if (found == callbacks().end()) { + dynamic_scope_close(scope); + return; + } + callback = std::move(found->second); + callbacks().erase(found); + } + auto args = + std::make_shared>(arrayFromNode(scope, root)); + dynamic_scope_close(scope); + callback->call([args](jsi::Runtime& rt, jsi::Function& function) { + std::vector values; + values.reserve(args->size()); + for (const auto& value : *args) { + values.push_back(jsiValueFromNodeValue(rt, value)); + } + const jsi::Value* first = values.data(); + function.call(rt, first, values.size()); + }); +} + +void rn_gpui_rust_callback_drop(uint64_t id) { + std::lock_guard lock(callbacksMutex()); + callbacks().erase(id); +} + +void rn_gpui_rust_module_emit( + uint64_t handle, + rust::Str event, + bool has_value, + uint32_t scope, + uint32_t root) { + auto module = moduleFor(handle); + if (!module) { + dynamic_scope_close(scope); + return; + } + const std::string name(event); + if (has_value) { + module->emitValueEvent(name, nodeValueFromScope(scope, root)); + } else { + module->emitVoidEvent(name); + } + dynamic_scope_close(scope); +} + +void rn_gpui_rust_module_send_event( + uint64_t handle, + rust::Str name, + uint32_t scope, + uint32_t root) { + auto module = moduleFor(handle); + if (!module) { + dynamic_scope_close(scope); + return; + } + module->sendDeviceEvent(std::string(name), nodeValueFromScope(scope, root)); + dynamic_scope_close(scope); +} + +void rn_gpui_rust_module_clear_pending() { + { + std::lock_guard lock(pendingMutex()); + pendingCalls().clear(); + } + { + std::lock_guard lock(callbacksMutex()); + callbacks().clear(); + } +} + +void install_rust_turbo_modules() { + gpuiModuleHooks().turboModules.push_back( + [](const std::string& name, const std::shared_ptr& jsInvoker) + -> std::shared_ptr { + const uint64_t handle = rn_gpui_rust_module_create(rust::Str(name)); + if (handle == 0) { + return nullptr; + } + auto module = std::make_shared( + name, + jsInvoker, + handle, + rn_gpui_rust_module_methods(handle), + rn_gpui_rust_module_events(handle)); + module->start(); + return module; + }); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiRustTurboModules.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiRustTurboModules.h new file mode 100644 index 0000000000..e0ecf5161f --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiRustTurboModules.h @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include + +namespace rngpui { + +/// Installs the provider that serves Rust TurboModules by name. Called once, +/// from the host, before any module is asked for. +void install_rust_turbo_modules(); + +/// Settles the JS promise of a Rust module call with the value built in +/// `scope` at `root`. `request` identifies the promise; an unknown request is +/// discarded, which is what a reload leaves behind. +void rn_gpui_rust_module_result( + int32_t request, + bool ok, + uint32_t scope, + uint32_t root, + rust::String code, + rust::String message); + +/// Calls a JS function a Rust module kept; `scope`/`root` hold its arguments. +void rn_gpui_rust_callback_invoke(uint64_t id, uint32_t scope, uint32_t root); + +void rn_gpui_rust_callback_drop(uint64_t id); + +/// Emits a typed spec event; `has_value` says whether `scope`/`root` carry a +/// payload. +void rn_gpui_rust_module_emit( + uint64_t handle, + rust::Str event, + bool has_value, + uint32_t scope, + uint32_t root); + +void rn_gpui_rust_module_send_event( + uint64_t handle, + rust::Str name, + uint32_t scope, + uint32_t root); + +/// Drops the promises and JS functions of the runtime that is going away. +void rn_gpui_rust_module_clear_pending(); + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiStartupSignpost.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiStartupSignpost.cpp new file mode 100644 index 0000000000..df82691383 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiStartupSignpost.cpp @@ -0,0 +1,47 @@ +#include "GpuiStartupSignpost.h" + +#if defined(__APPLE__) +#include +#include +#endif + +namespace rngpui { + +#if defined(__APPLE__) +namespace { + +/// One signpost log for every stage, created once. +os_log_t startupLog() { + static os_log_t log = os_log_create("org.reactnativegpui.startup", "startup"); + return log; +} + +} // namespace +#endif + +void rn_gpui_startup_signpost(::rust::Str stage, std::uint64_t elapsed_ms) { +#if defined(__APPLE__) + os_log_t log = startupLog(); + if (log == nullptr || !os_signpost_enabled(log)) { + return; + } + // One id, so every stage of a launch belongs to the same interval group. + static os_signpost_id_t id = os_signpost_id_generate(log); + // The name must be a string literal, and the format's arguments follow it. + // The stage goes in the format string with `%.*s`, bounded by its length, so + // it does not have to be constant and cannot run past its own bytes. + os_signpost_event_emit( + log, + id, + "startup stage", + "+%llu %.*s", + elapsed_ms, + static_cast(stage.size()), + stage.data()); +#else + (void)stage; + (void)elapsed_ms; +#endif +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiStartupSignpost.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiStartupSignpost.h new file mode 100644 index 0000000000..73170854ee --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiStartupSignpost.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include "rust/cxx.h" + +namespace rngpui { + +/// Emit an `os_signpost` point event for a startup stage. +/// +/// This lives in C++ because `os/signpost.h` declares its API as macros over a +/// variadic implementation function, and getting that ABI right by hand in Rust +/// is exactly the kind of thing that crashes on the first call. Here the macro +/// expands as Apple intends and there is nothing to transcribe. +/// +/// A no-op on platforms without signposts. +void rn_gpui_startup_signpost(::rust::Str stage, std::uint64_t elapsed_ms); + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTextMeasure.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTextMeasure.h new file mode 100644 index 0000000000..87e1f8e0a1 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTextMeasure.h @@ -0,0 +1,30 @@ +// The TextLayoutManager override is compiled as part of ReactCommon, which +// must not see the cxx bridge headers, so it calls through this plain +// function-pointer table. +#pragma once + +#include +#include + +namespace rngpui { +struct GpuiTextSize { + float width; + float height; +}; + +struct GpuiTextMeasureTable { + GpuiTextSize (*measureParagraph)( + const facebook::react::AttributedString& text, + const facebook::react::ParagraphAttributes& attributes, + float maxWidth, + float maxHeight); +}; + +// Defined in ReactCommonOverrides/.../textlayoutmanager/platform/cxx/.../ +// TextLayoutManager.cpp. Installed once by GpuiHost before any surface is +// started. Measuring before that falls back to a zero-sized paragraph, as the +// Rust side does when no measure handler is registered. +void setGpuiTextMeasureTable(const GpuiTextMeasureTable& table); +const GpuiTextMeasureTable* gpuiTextMeasureTable(); + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTurboModule.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTurboModule.cpp new file mode 100644 index 0000000000..6e0d4b5ff5 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTurboModule.cpp @@ -0,0 +1,171 @@ +#include "GpuiTurboModule.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#else +#include +#endif + +#include "rn-gpui-sys/src/lib.rs.h" // cxx bridge + +namespace rngpui { +namespace { + +/// Best-effort thread name: the platform truncates or rejects names it cannot +/// store, and a failure only costs a debugging aid. +void nameCurrentThread(const std::string& name) { +#if defined(_WIN32) + using SetThreadDescriptionFn = HRESULT(WINAPI*)(HANDLE, PCWSTR); + static const auto setThreadDescription = []() -> SetThreadDescriptionFn { + const auto module = GetModuleHandleW(L"kernel32.dll"); + return module != nullptr + ? reinterpret_cast( + GetProcAddress(module, "SetThreadDescription")) + : nullptr; + }(); + if (setThreadDescription == nullptr) { + return; + } + const auto size = + MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, nullptr, 0); + if (size <= 1) { + return; + } + std::wstring wide(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, wide.data(), size); + setThreadDescription(GetCurrentThread(), wide.c_str()); +#elif defined(__APPLE__) + pthread_setname_np(name.c_str()); +#else + pthread_setname_np(pthread_self(), name.substr(0, 15).c_str()); +#endif +} + +std::mutex mainTasksMutex; +std::unordered_map> mainTasks; +std::atomic nextMainTaskId{1}; + +} // namespace + +struct ModuleQueue::State { + std::mutex mutex; + std::condition_variable ready; + std::deque> tasks; + bool started{false}; + bool stopped{false}; + std::thread worker; +}; + +ModuleQueue::ModuleQueue(std::string threadName) + : state_(std::make_shared()), threadName_(std::move(threadName)) {} + +ModuleQueue::~ModuleQueue() { + stop(); +} + +void ModuleQueue::stop() { + if (!state_) { + return; + } + auto state = std::move(state_); + { + std::lock_guard lock(state->mutex); + state->stopped = true; + state->tasks.clear(); + } + state->ready.notify_all(); + if (!state->worker.joinable()) { + return; + } + if (state->worker.get_id() == std::this_thread::get_id()) { + // A task stops the queue of a module the JS thread released while the call + // ran. The worker sees `stopped` and exits; joining would deadlock on + // itself. + state->worker.detach(); + return; + } + state->worker.join(); +} + +void ModuleQueue::post(std::function task) { + bool startWorker = false; + { + std::lock_guard lock(state_->mutex); + if (state_->stopped) { + return; + } + state_->tasks.push_back(std::move(task)); + if (!state_->started) { + state_->started = true; + startWorker = true; + } + } + if (startWorker) { + // The worker owns its state through the shared pointer, never through this. + state_->worker = std::thread([state = state_, name = threadName_] { + nameCurrentThread(name); + for (;;) { + std::function next; + { + std::unique_lock lock(state->mutex); + state->ready.wait(lock, [&state] { + return state->stopped || !state->tasks.empty(); + }); + if (state->stopped) { + return; + } + next = std::move(state->tasks.front()); + state->tasks.pop_front(); + } + try { + next(); + } catch (const std::exception& error) { + LOG(ERROR) << "module queue task failed: " << error.what(); + } + } + }); + } + state_->ready.notify_one(); +} + +void dispatchMain(std::function task) { + const auto id = nextMainTaskId.fetch_add(1, std::memory_order_relaxed); + { + std::lock_guard lock(mainTasksMutex); + mainTasks.emplace(id, std::move(task)); + } + rn_gpui_run_on_main(id); +} + +void rn_gpui_run_main_task(uint64_t id) { + std::function task; + { + std::lock_guard lock(mainTasksMutex); + auto entry = mainTasks.find(id); + if (entry == mainTasks.end()) { + return; + } + task = std::move(entry->second); + mainTasks.erase(entry); + } + try { + task(); + } catch (const std::exception& error) { + LOG(ERROR) << "main thread task failed: " << error.what(); + } +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTurboModule.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTurboModule.h new file mode 100644 index 0000000000..8948454e6c --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/GpuiTurboModule.h @@ -0,0 +1,635 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rngpui { + +namespace jsi = facebook::jsi; + +/// A JS function a module method may call from any thread. Call it at most +/// once, as an Objective-C response block is called. +template +using Callback = facebook::react::AsyncCallback; + +/// The thread a module's `void` and `Promise` methods run on; methods that +/// return a value always run on the JS thread. +enum class ModuleThread { + Own, + Main, + JS, +}; + +/// A serial task queue. `post` is callable from any thread; the worker starts +/// on the first post and stops when the queue is destroyed. +class ModuleQueue { + public: + explicit ModuleQueue(std::string threadName); + ~ModuleQueue(); + + ModuleQueue(const ModuleQueue&) = delete; + ModuleQueue& operator=(const ModuleQueue&) = delete; + + void stop(); + + void post(std::function task); + + private: + struct State; + std::shared_ptr state_; + std::string threadName_; +}; + +/// Runs `task` on the GPUI main thread. The task is dropped, with a warning +/// logged once, while no main-thread handler is installed. +void dispatchMain(std::function task); + +/// The promise a module-thread method settles. It is created on the JS thread +/// by the spec invoker, copyable, and `resolve`/`reject` may be called from any +/// thread; only the first settlement has an effect. +template +class Promise { + static_assert(sizeof...(T) <= 1, "Promise takes at most one value type"); + + public: + Promise( + jsi::Runtime& rt, + const std::shared_ptr& jsInvoker); + + void resolve(T... value); + + void reject(std::string message); + + /// The error carries `code` as its `code` property. + void reject(std::string code, std::string message); + + /// The JS promise; only call this on the JS thread, before the module method + /// can settle it. + jsi::Object get(jsi::Runtime& rt) const; + + private: + struct PromiseHolder : facebook::react::LongLivedObject { + PromiseHolder(jsi::Runtime& rt, jsi::Object p) + : LongLivedObject(rt), promise(std::move(p)) {} + + jsi::Object promise; + }; + + struct SharedState { + ~SharedState() { + if (auto holder = promiseHolder.lock()) { + holder->allowRelease(); + } + } + + std::mutex mutex; + std::weak_ptr promiseHolder; + std::optional> resolve; + std::optional> reject; + }; + + void settleReject(std::optional code, std::string message); + + std::shared_ptr state_; +}; + +template +Promise::Promise( + jsi::Runtime& rt, + const std::shared_ptr& jsInvoker) + : state_(std::make_shared()) { + auto constructor = rt.global().getPropertyAsFunction(rt, "Promise"); + auto promise = constructor.callAsConstructor( + rt, + facebook::react::bridging::toJs( + rt, + // The executor runs synchronously, so capturing `this` is safe. + [this]( + facebook::react::AsyncCallback resolve, + const facebook::react::AsyncCallback<>& reject) { + state_->resolve = std::move(resolve); + state_->reject = std::move(reject); + }, + jsInvoker)); + + auto holder = std::make_shared(rt, promise.asObject(rt)); + facebook::react::LongLivedObjectCollection::get(rt).add(holder); + state_->promiseHolder = holder; +} + +template +void Promise::resolve(T... value) { + std::optional> callback; + { + std::lock_guard lock(state_->mutex); + if (!state_->resolve) { + return; + } + callback = *state_->resolve; + state_->resolve.reset(); + state_->reject.reset(); + } + callback->call(std::forward(value)...); +} + +template +void Promise::reject(std::string message) { + settleReject(std::nullopt, std::move(message)); +} + +template +void Promise::reject(std::string code, std::string message) { + settleReject(std::move(code), std::move(message)); +} + +template +void Promise::settleReject( + std::optional code, + std::string message) { + std::optional> callback; + { + std::lock_guard lock(state_->mutex); + if (!state_->reject) { + return; + } + callback = *state_->reject; + state_->resolve.reset(); + state_->reject.reset(); + } + callback->call([code = std::move(code), message = std::move(message)]( + jsi::Runtime& rt, jsi::Function& reject) { + auto error = jsi::Value(rt, jsi::JSError(rt, message).value()); + if (code) { + error.asObject(rt).setProperty( + rt, "code", jsi::String::createFromUtf8(rt, *code)); + } + reject.call(rt, error); + }); +} + +template +jsi::Object Promise::get(jsi::Runtime& rt) const { + if (auto holder = state_->promiseHolder.lock()) { + return jsi::Value(rt, holder->promise).asObject(rt); + } + throw jsi::JSError(rt, "the promise was invalidated by a reload"); +} + +namespace detail { + +template +struct MethodTraits { + static_assert( + !std::is_same_v, + "a spec method must be a plain member function taking the spec's " + "arguments; const and noexcept qualifiers are not supported"); +}; + +template +struct MethodTraits { + using Class = ClassT; + using Return = ReturnT; + using Args = std::tuple; + static constexpr size_t kArity = sizeof...(ArgsT); + + template + using Arg = std::tuple_element_t>; +}; + +template +constexpr bool argsAreNotJsi(std::index_sequence) { + return ( + !facebook::react::bridging::is_jsi_v< + std::decay_t>> && + ...); +} + +} // namespace detail + +/// The base of every generated spec class. `T` is the implementation, which +/// declares one member function per spec method and may set `kThread`. +template +class TurboModuleBase : public facebook::react::TurboModule, + public std::enable_shared_from_this { + public: + static constexpr ModuleThread kThread = ModuleThread::Own; + + protected: + TurboModuleBase( + std::string name, + std::shared_ptr jsInvoker) + : facebook::react::TurboModule(std::move(name), std::move(jsInvoker)) {} + + /// A method declared as `R name(jsi::Runtime&, Args...)`, run on the JS + /// thread. + template + void valueMethod(const char* name) { + using Traits = detail::MethodTraits; + registerName(name); + methodMap_[name] = MethodMetadata{ + .argCount = Traits::kArity - 1, .invoker = &invokeValue}; + } + + /// A method declared as `void name(Args...)`, run on `T::kThread`. + template + void voidMethod(const char* name) { + using Traits = detail::MethodTraits; + constexpr bool kOnJs = T::kThread == ModuleThread::JS; + if constexpr (!kOnJs) { + static_assert( + detail::argsAreNotJsi( + std::make_index_sequence{}), + "a void method that does not run on the JS thread cannot take a JSI " + "type: convert the argument to a C++ type such as folly::dynamic"); + } + registerName(name); + methodMap_[name] = MethodMetadata{ + .argCount = Traits::kArity - (kOnJs ? 1 : 0), + .invoker = &invokeVoid}; + } + + /// A method declared as `void name(Args..., rngpui::Promise)`, run on + /// `T::kThread`. + template + void promiseMethod(const char* name) { + using Traits = detail::MethodTraits; + constexpr bool kOnJs = T::kThread == ModuleThread::JS; + if constexpr (!kOnJs) { + static_assert( + detail::argsAreNotJsi( + std::make_index_sequence{}), + "a promise method that does not run on the JS thread cannot take a " + "JSI type: convert the argument to a C++ type such as " + "folly::dynamic"); + } + registerName(name); + methodMap_[name] = MethodMetadata{ + .argCount = Traits::kArity - 1 - (kOnJs ? 1 : 0), + .invoker = &invokePromise}; + } + + /// `V...` is the emitter's payload: `jsi::Value` for an event that carries a + /// value, empty for one that does not. + template + void addEvent(const char* name) { + eventEmitterMap_[name] = + std::make_shared>(); + } + + template + void emitEvent(const char* name, V value) { + auto entry = eventEmitterMap_.find(name); + if (entry == eventEmitterMap_.end()) { + return; + } + // A hand-written module can pair a payload-less emitter with a value; a + // mismatched cast would be undefined, so the event is dropped instead. + auto emitter = std::dynamic_pointer_cast< + facebook::react::AsyncEventEmitter>(entry->second); + if (!emitter) { + return; + } + emitter->emit( + [jsInvoker = jsInvoker_, + value = std::move(value)](jsi::Runtime& rt) -> jsi::Value { + return facebook::react::bridging::toJs(rt, value, jsInvoker); + }); + } + + void emitEvent(const char* name) { + auto entry = eventEmitterMap_.find(name); + if (entry == eventEmitterMap_.end()) { + return; + } + auto emitter = + std::dynamic_pointer_cast>( + entry->second); + if (!emitter) { + return; + } + emitter->emit(); + } + + /// For a module used through `NativeEventEmitter`. + void sendEvent(std::string name, folly::dynamic body = nullptr) { + emitDeviceEvent( + name, + [body = std::move(body)]( + jsi::Runtime& rt, std::vector& args) { + if (!body.isNull()) { + args.emplace_back(jsi::valueFromDynamic(rt, body)); + } + }); + } + + private: + /// The method name for arity errors. Every instance of `T` registers the same + /// name, so the last write is the value every reader sees. + template + static std::atomic& registeredName() { + static std::atomic name{nullptr}; + return name; + } + + template + static void registerName(const char* name) { + registeredName().store(name, std::memory_order_relaxed); + } + + template + static std::string arityMessage( + const facebook::react::TurboModule& base, + size_t expected, + size_t count) { + const auto& self = static_cast&>(base); + const char* name = registeredName().load(std::memory_order_relaxed); + return self.name_ + "." + (name != nullptr ? name : "") + + " expects " + std::to_string(expected) + " arguments, got " + + std::to_string(count); + } + + /// The number of JS arguments the method needs: a parameter the + /// implementation declares as `std::optional` may be omitted, so the last + /// required parameter decides. + template + static constexpr size_t requiredCount(std::index_sequence) { + size_t required = 0; + ((required = facebook::react::bridging::is_optional_v< + std::decay_t>> + ? required + : I + 1), + ...); + return required; + } + + /// `jsIndex` is past the end when the spec marks the parameter optional and + /// JS omitted it. + template + static std::decay_t> convertArg( + jsi::Runtime& rt, + const jsi::Value* args, + size_t count, + const std::shared_ptr& jsInvoker, + size_t jsIndex) { + using Arg = std::decay_t>; + if (jsIndex >= count) { + if constexpr (facebook::react::bridging::is_optional_v) { + return Arg{}; + } else { + // Unreachable: requiredCount() rejected the call already. + throw jsi::JSError(rt, "a required argument is missing"); + } + } + return facebook::react::bridging::fromJs(rt, args[jsIndex], jsInvoker); + } + + /// Converts `Args[Offset..]`; the JS arguments start at `args[0]`. + template + static auto convertArgs( + jsi::Runtime& rt, + const facebook::react::TurboModule& base, + const jsi::Value* args, + size_t count, + const std::shared_ptr& jsInvoker, + std::index_sequence) { + constexpr size_t kRequired = + requiredCount(std::index_sequence{}); + if (count < kRequired) { + throw jsi::JSError(rt, arityMessage(base, kRequired, count)); + } + return std::make_tuple( + convertArg(rt, args, count, jsInvoker, I)...); + } + + template + static jsi::Value invokeValue( + jsi::Runtime& rt, + facebook::react::TurboModule& base, + const jsi::Value* args, + size_t count) { + using Traits = detail::MethodTraits; + auto& self = static_cast(base); + auto converted = convertArgs( + rt, + base, + args, + count, + self.jsInvoker_, + std::make_index_sequence{}); + return std::apply( + [&](auto&&... values) -> jsi::Value { + return facebook::react::bridging::toJs( + rt, + (self.*Method)(rt, std::forward(values)...), + self.jsInvoker_); + }, + std::move(converted)); + } + + template + static jsi::Value invokeVoid( + jsi::Runtime& rt, + facebook::react::TurboModule& base, + const jsi::Value* args, + size_t count) { + using Traits = detail::MethodTraits; + constexpr bool kOnJs = T::kThread == ModuleThread::JS; + constexpr size_t kOffset = kOnJs ? 1 : 0; + auto& self = static_cast(base); + auto converted = convertArgs( + rt, + base, + args, + count, + self.jsInvoker_, + std::make_index_sequence{}); + if constexpr (kOnJs) { + std::apply( + [&](auto&&... values) { + (self.*Method)(rt, std::forward(values)...); + }, + std::move(converted)); + } else { + auto task = [weak = std::weak_ptr(self.weak_from_this()), + converted = std::move(converted)]() mutable { + auto module = weak.lock(); + if (!module) { + return; + } + try { + std::apply( + [&](auto&&... values) { + (module.get()->*Method)( + std::forward(values)...); + }, + std::move(converted)); + } catch (const std::exception& error) { + LOG(ERROR) << module->name_ << ": " << error.what(); + } + releaseOnJsThread(std::move(module)); + }; + self.post(std::move(task)); + } + return jsi::Value::undefined(); + } + + template + static jsi::Value invokePromise( + jsi::Runtime& rt, + facebook::react::TurboModule& base, + const jsi::Value* args, + size_t count) { + using Traits = detail::MethodTraits; + using PromiseType = + std::decay_t>; + constexpr bool kOnJs = T::kThread == ModuleThread::JS; + constexpr size_t kOffset = kOnJs ? 1 : 0; + auto& self = static_cast(base); + auto converted = convertArgs( + rt, + base, + args, + count, + self.jsInvoker_, + std::make_index_sequence{}); + PromiseType promise(rt, self.jsInvoker_); + auto task = [weak = std::weak_ptr(self.weak_from_this()), + converted = std::move(converted), + promise]() mutable { + auto module = weak.lock(); + if (!module) { + promise.reject("the module was released before the call ran"); + return; + } + try { + std::apply( + [&](auto&&... values) { + (module.get()->*Method)( + std::forward(values)..., promise); + }, + std::move(converted)); + } catch (const std::exception& error) { + promise.reject(error.what()); + } + releaseOnJsThread(std::move(module)); + }; + if constexpr (kOnJs) { + task(); + } else { + self.post(std::move(task)); + } + return promise.get(rt); + } + + /// A module owns JSI state, so it has to be destroyed on the JS thread while + /// the runtime is alive. A task that holds the last reference means the JS + /// owner released the module while the call ran (a reload), so no JS thread + /// can destroy it any more. The queue is stopped first, so its worker thread + /// does not outlive the module, and the reference is then kept until the + /// process exits: releasing it here would release JSI state from a module + /// thread, after the runtime is gone. + static void releaseOnJsThread(std::shared_ptr module) { + if (module.use_count() > 1) { + return; + } + module->stopQueue(); + std::lock_guard lock(parkedModulesMutex()); + parkedModules().push_back(std::move(module)); + } + + // Both are leaked on purpose: destroying a parked module at exit would + // release its JSI state after the runtime is gone, which is the crash parking + // it avoids. + static std::mutex& parkedModulesMutex() { + static auto* mutex = new std::mutex(); + return *mutex; + } + + static std::vector>& parkedModules() { + static auto* parked = new std::vector>(); + return *parked; + } + + /// Stops this module's queue, if it has one. + void stopQueue() { + if (queue_) { + queue_->stop(); + } + } + + /// Runs `task` on `T::kThread`. JS-thread methods run inline in their + /// invoker, so they never reach this. + void post(std::function task) { + if constexpr (T::kThread == ModuleThread::Main) { + dispatchMain(std::move(task)); + } else { + if (!queue_) { + queue_ = std::make_unique("rn.module." + name_); + } + queue_->post(std::move(task)); + } + } + + std::unique_ptr queue_; +}; + +/// Returns a new Module when `name` is the module's registered name, otherwise +/// nullptr. +template +std::shared_ptr provideTurboModule( + const std::string& name, + std::shared_ptr jsInvoker) { + if (name != Module::kModuleName) { + return nullptr; + } + return std::make_shared(std::move(jsInvoker)); +} + +} // namespace rngpui + +namespace facebook::react { + +/// An `ArrayBuffer` spec type is `std::vector`, so a method that runs +/// off the JS thread gets bytes it owns rather than a runtime-bound object. +template <> +struct Bridging> { + static std::vector fromJs(jsi::Runtime& rt, const jsi::Object& obj) { + if (!obj.isArrayBuffer(rt)) { + throw jsi::JSError(rt, "expected an ArrayBuffer"); + } + const jsi::ArrayBuffer buffer = obj.getArrayBuffer(rt); + const uint8_t* data = buffer.data(rt); + return std::vector(data, data + buffer.size(rt)); + } + + static jsi::Value toJs(jsi::Runtime& rt, const std::vector& bytes) { + return { + rt, + rt.createArrayBuffer( + std::make_shared( + bytes))}; + } +}; + +} // namespace facebook::react diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/MountBatchBuilder.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/MountBatchBuilder.cpp new file mode 100644 index 0000000000..3d20f996a6 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/MountBatchBuilder.cpp @@ -0,0 +1,441 @@ +#include "MountBatchBuilder.h" +#include "GpuiHostComponentsCpp.h" +#include "GpuiModules.h" +#include "GpuiTextMeasure.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace rngpui { +using namespace facebook::react; + +uint32_t argb(const SharedColor& color) { + if (!color) { + return 0; + } + auto components = colorComponentsFromColor(color); + auto to8 = [](float v) { + return static_cast(std::lround(v * 255.f)) & 0xFF; + }; + return (to8(components.alpha) << 24) | (to8(components.red) << 16) | + (to8(components.green) << 8) | to8(components.blue); +} + +TextFragmentFlat flattenTextFragment( + const TextAttributes& attributes, + const std::string& text) { + TextFragmentFlat out{}; + out.text = text; + out.font_family = attributes.fontFamily; + out.font_size = std::isnan(attributes.fontSize) ? 14.f : attributes.fontSize; + out.font_weight = attributes.fontWeight + ? static_cast(*attributes.fontWeight) + : 400; + out.italic = + attributes.fontStyle && *attributes.fontStyle != FontStyle::Normal; + out.color = argb(attributes.foregroundColor); + out.line_height = + std::isnan(attributes.lineHeight) ? 0.f : attributes.lineHeight; + out.letter_spacing = + std::isnan(attributes.letterSpacing) ? 0.f : attributes.letterSpacing; + if (attributes.textDecorationLineType) { + switch (*attributes.textDecorationLineType) { + case TextDecorationLineType::Underline: + out.underline = true; + break; + case TextDecorationLineType::Strikethrough: + out.strikethrough = true; + break; + case TextDecorationLineType::UnderlineStrikethrough: + out.underline = true; + out.strikethrough = true; + break; + case TextDecorationLineType::None: + break; + } + } + return out; +} + +static ViewPropsFlat flattenView(const ShadowView& shadowView) { + ViewPropsFlat out{}; + // Non-view components must remain opaque; a value-initialized flat struct + // otherwise silently turns an unrecognized props type fully transparent. + out.opacity = 1.f; + out.display_none = shadowView.layoutMetrics.displayType == DisplayType::None; + auto props = std::dynamic_pointer_cast(shadowView.props); + if (!props) { + return out; + } + + out.opacity = props->opacity; + out.background_color = argb(props->backgroundColor); + + // layoutMetrics.borderWidth is already fully resolved per edge; the cascaded + // props->getBorderWidths() would need the same resolve() dance as the colors. + const auto& layoutMetrics = shadowView.layoutMetrics; + out.border_widths = { + layoutMetrics.borderWidth.left, + layoutMetrics.borderWidth.top, + layoutMetrics.borderWidth.right, + layoutMetrics.borderWidth.bottom}; + out.content_insets = { + layoutMetrics.contentInsets.left, + layoutMetrics.contentInsets.top, + layoutMetrics.contentInsets.right, + layoutMetrics.contentInsets.bottom}; + + auto colors = props->borderColors.resolve(/*isRTL*/ false, SharedColor{}); + out.border_colors = { + argb(colors.left), + argb(colors.top), + argb(colors.right), + argb(colors.bottom)}; + auto borderStyles = props->borderStyles.resolve(false, BorderStyle::Solid); + out.border_styles = { + static_cast(borderStyles.left), + static_cast(borderStyles.top), + static_cast(borderStyles.right), + static_cast(borderStyles.bottom)}; + out.outline_color = argb(props->outlineColor); + out.outline_width = props->outlineWidth; + out.outline_offset = props->outlineOffset; + out.outline_style = static_cast(props->outlineStyle); + + // BorderRadii is RectangleCorners; each corner resolves to px + // against the frame's smaller side (percentage radii). + auto radii = props->borderRadii.resolve(false, ValueUnit{0, UnitType::Point}); + auto px = [&](const ValueUnit& v) { + return v.resolve(std::min( + layoutMetrics.frame.size.width, layoutMetrics.frame.size.height)); + }; + out.border_radii = { + px(radii.topLeft), + px(radii.topRight), + px(radii.bottomRight), + px(radii.bottomLeft)}; + + out.overflow_hidden = props->getClipsContentToBounds(); + out.pointer_events = static_cast(props->pointerEvents); + // `Cursor` is `enum class Cursor : uint8_t` in this exact order, starting at + // Auto = 0; `style::cursor_style` in `rn-gpui` spells the same ordinals out. + out.cursor = static_cast(props->cursor); + + for (const auto& shadow : props->boxShadow) { + out.shadows.push_back( + {shadow.offsetX, + shadow.offsetY, + shadow.blurRadius, + shadow.spreadDistance, + argb(shadow.color), + shadow.inset}); + } + + out.test_id = props->testId; + out.accessibility_native_id = props->nativeId; + out.accessible = props->accessible; + out.accessibility_label = props->accessibilityLabel; + out.accessibility_hint = props->accessibilityHint; + out.accessibility_language = props->accessibilityLanguage; + out.accessibility_role = props->accessibilityRole; + if (props->role != Role::None) { + out.accessibility_role = toString(props->role); + } + out.accessibility_traits = static_cast(props->accessibilityTraits); + for (const auto& tag : props->accessibilityOrder) { + out.accessibility_order.push_back(tag); + } + for (const auto& nativeId : props->accessibilityLabelledBy.value) { + out.accessibility_labelled_by.push_back(nativeId); + } + out.accessibility_hidden = props->accessibilityElementsHidden; + out.accessibility_modal = props->accessibilityViewIsModal; + out.accessibility_ignores_invert_colors = + props->accessibilityIgnoresInvertColors; + out.accessibility_large_content_enabled = + props->accessibilityShowsLargeContentViewer; + out.accessibility_large_content_title = props->accessibilityLargeContentTitle; + out.accessibility_responds_to_interaction = + props->accessibilityRespondsToUserInteraction; + out.accessibility_has_action_listener = props->onAccessibilityAction; + out.accessibility_has_tap_listener = props->onAccessibilityTap; + out.accessibility_has_escape_listener = props->onAccessibilityEscape; + out.accessibility_has_magic_tap_listener = props->onAccessibilityMagicTap; + out.accessibility_important = + static_cast(props->importantForAccessibility); + out.accessibility_live_region = + static_cast(props->accessibilityLiveRegion); + for (const auto& action : props->accessibilityActions) { + out.accessibility_actions.push_back( + {action.name, action.label.value_or("")}); + } + out.accessibility_disabled = (out.accessibility_traits & (1u << 8)) != 0; + out.accessibility_selected = (out.accessibility_traits & (1u << 3)) != 0; + const auto& accessibilityValue = props->accessibilityValue; + if (accessibilityValue.min) { + out.accessibility_has_value_min = true; + out.accessibility_value_min = *accessibilityValue.min; + } + if (accessibilityValue.max) { + out.accessibility_has_value_max = true; + out.accessibility_value_max = *accessibilityValue.max; + } + if (accessibilityValue.now) { + out.accessibility_has_value_now = true; + out.accessibility_value_now = *accessibilityValue.now; + } + if (accessibilityValue.text) { + out.accessibility_value_text = *accessibilityValue.text; + } + if (props->accessibilityState) { + const auto& state = *props->accessibilityState; + out.accessibility_disabled = + state.disabled || (out.accessibility_traits & (1u << 8)) != 0; + out.accessibility_selected = + state.selected || (out.accessibility_traits & (1u << 3)) != 0; + out.accessibility_busy = state.busy; + switch (state.checked) { + case AccessibilityState::Unchecked: + out.accessibility_checked = 1; + break; + case AccessibilityState::Checked: + out.accessibility_checked = 2; + break; + case AccessibilityState::Mixed: + out.accessibility_checked = 3; + break; + case AccessibilityState::None: + break; + } + if (state.expanded) { + out.accessibility_expanded = *state.expanded ? 1 : 0; + } else { + out.accessibility_expanded = -1; + } + } else { + out.accessibility_expanded = -1; + } + if (auto platformProps = + std::dynamic_pointer_cast(shadowView.props)) { + out.focusable = platformProps->focusable; + out.desktop_drag_region = platformProps->desktopDragRegion; + out.on_context_menu = platformProps->onContextMenu; + } + + const auto transform = props->resolveTransform(layoutMetrics); + out.transform = transform.matrix; + out.has_transform = !(transform == Transform::Identity()); + return out; +} + +static ParagraphFlat flattenParagraph(const ShadowView& shadowView) { + ParagraphFlat out{}; + auto state = std::dynamic_pointer_cast>( + shadowView.state); + if (!state) { + return out; + } + + const auto& data = state->getData(); + const auto& fragments = data.attributedString.getFragments(); + for (const auto& fragment : fragments) { + out.fragments.push_back( + flattenTextFragment(fragment.textAttributes, fragment.string)); + } + + // Text alignment lives on TextAttributes, not on ParagraphAttributes: take + // it from the first fragment (they are homogeneous in practice). The + // TextAlignment enum values already match the ParagraphFlat encoding. + if (!fragments.empty() && fragments.front().textAttributes.alignment) { + out.text_align = + static_cast(*fragments.front().textAttributes.alignment); + } + + const auto& attributes = data.paragraphAttributes; + out.max_lines = attributes.maximumNumberOfLines; + // EllipsizeMode is {Clip=0, Head=1, Tail=2, Middle=3} but the GPUI slot + // encodes {0 tail, 1 head, 2 middle, 3 clip}. + switch (attributes.ellipsizeMode) { + case EllipsizeMode::Clip: + out.ellipsize = 3; + break; + case EllipsizeMode::Head: + out.ellipsize = 1; + break; + case EllipsizeMode::Tail: + out.ellipsize = 0; + break; + case EllipsizeMode::Middle: + out.ellipsize = 2; + break; + } + return out; +} + +static ParagraphFlat flattenInputText( + const AttributedString& attributedString) { + ParagraphFlat out{}; + for (const auto& fragment : attributedString.getFragments()) { + out.fragments.push_back( + flattenTextFragment(fragment.textAttributes, fragment.string)); + } + return out; +} + +MountOp buildMountOp(const ShadowViewMutation& mutation) { + MountOp op{}; + op.kind = static_cast(mutation.type); + op.parent_tag = mutation.parentTag; + op.index = mutation.index; + + const ShadowView& shadowView = (mutation.type == ShadowViewMutation::Delete || + mutation.type == ShadowViewMutation::Remove) + ? mutation.oldChildShadowView + : mutation.newChildShadowView; + + op.tag = shadowView.tag; + op.component = std::string(shadowView.componentName); + const std::string componentName(op.component.c_str()); + { + const auto& registered = + gpuiExtensionHooks().componentProps.find(componentName); + if (registered != gpuiExtensionHooks().componentProps.end() && + shadowView.props) { + op.extension_props_json = registered->second(componentName, shadowView); + } + } + op.host_index = -1; + op.host_props_handle = 0; + op.host_emitter_handle = 0; + op.host_changed = 0; + if (rngpui::anyHostComponents() && + (mutation.type == ShadowViewMutation::Create || + mutation.type == ShadowViewMutation::Update)) { + if (const auto* entry = + rngpui::hostComponentEntry(shadowView.componentHandle)) { + op.host_index = static_cast(entry->index); + if (mutation.type == ShadowViewMutation::Create) { + op.host_props_handle = rngpui::makeHostPropsHandle(shadowView.props); + op.host_emitter_handle = + rngpui::makeHostEmitterHandle(shadowView.eventEmitter); + } else { + const auto& before = mutation.oldChildShadowView.props; + const auto& after = mutation.newChildShadowView.props; + if (before && after && before.get() != after.get()) { + uint64_t inlineWords[2] = {0, 0}; + std::vector heapWords; + uint64_t* words = inlineWords; + if (entry->changeWords > 2) { + heapWords.assign(entry->changeWords, 0); + words = heapWords.data(); + } + entry->changed(*before, *after, words); + bool any = false; + for (uint32_t word = 0; word < entry->changeWords; ++word) { + any = any || words[word] != 0; + } + if (any) { + op.host_props_handle = rngpui::makeHostPropsHandle(after); + op.host_changed = words[0]; + for (uint32_t word = 1; word < entry->changeWords; ++word) { + op.host_changed_more.push_back(words[word]); + } + } + } + } + } + } + if (op.component == "NativeViewHost") { + if (auto props = std::dynamic_pointer_cast( + shadowView.props)) { + op.native_view_name = props->viewName; + op.native_view_props = props->nativePropsJson; + } + } + + const auto& frame = shadowView.layoutMetrics.frame; + op.frame = { + frame.origin.x, frame.origin.y, frame.size.width, frame.size.height}; + op.layout_changed = mutation.type == ShadowViewMutation::Create || + (mutation.type == ShadowViewMutation::Update && + mutation.oldChildShadowView.layoutMetrics.frame != + mutation.newChildShadowView.layoutMetrics.frame); + + if (mutation.type == ShadowViewMutation::Create || + mutation.type == ShadowViewMutation::Update) { + op.props_changed = mutation.type == ShadowViewMutation::Create || + mutation.oldChildShadowView.props != + mutation.newChildShadowView.props || + mutation.oldChildShadowView.state != mutation.newChildShadowView.state; + op.view = flattenView(shadowView); + if (op.component == "Paragraph") { + op.has_paragraph = true; + op.paragraph = flattenParagraph(shadowView); + } + if (op.component == "TextInput") { + if (auto state = + std::dynamic_pointer_cast>( + shadowView.state)) { + op.has_paragraph = true; + op.paragraph = + flattenInputText(state->getData().attributedStringBox.getValue()); + } + } + } + return op; +} + +namespace { +GpuiTextSize measureParagraphThroughRust( + const AttributedString& text, + const ParagraphAttributes& attributes, + float maxWidth, + float maxHeight) { + ParagraphFlat paragraph = flattenInputText(text); + + // Alignment lives on TextAttributes, not on ParagraphAttributes; the + // TextAlignment enum values already match the ParagraphFlat encoding. + const auto& fragments = text.getFragments(); + if (!fragments.empty() && fragments.front().textAttributes.alignment) { + paragraph.text_align = + static_cast(*fragments.front().textAttributes.alignment); + } + paragraph.max_lines = attributes.maximumNumberOfLines; + switch (attributes.ellipsizeMode) { + case EllipsizeMode::Clip: + paragraph.ellipsize = 3; + break; + case EllipsizeMode::Head: + paragraph.ellipsize = 1; + break; + case EllipsizeMode::Middle: + paragraph.ellipsize = 2; + break; + case EllipsizeMode::Tail: + paragraph.ellipsize = 0; + break; + } + + const auto result = rn_gpui_measure_text(paragraph, maxWidth, maxHeight); + return GpuiTextSize{.width = result.width, .height = result.height}; +} +} // namespace + +void installGpuiTextMeasureTable() { + setGpuiTextMeasureTable( + GpuiTextMeasureTable{.measureParagraph = &measureParagraphThroughRust}); +} +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/MountBatchBuilder.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/MountBatchBuilder.h new file mode 100644 index 0000000000..1f9956db0d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/MountBatchBuilder.h @@ -0,0 +1,24 @@ +#pragma once + +#include "rn-gpui-sys/src/lib.rs.h" // cxx bridge + +#include +#include +#include + +#include +#include + +namespace rngpui { +MountOp buildMountOp(const facebook::react::ShadowViewMutation& mutation); + +// Shared flattening helpers. The text measurement override +// reuses these so React and GPUI describe text identically. +uint32_t argb(const facebook::react::SharedColor& color); +TextFragmentFlat flattenTextFragment( + const facebook::react::TextAttributes& attributes, + const std::string& text); + +void installGpuiTextMeasureTable(); + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/android-shim/README.md b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/android-shim/README.md new file mode 100644 index 0000000000..167f1d210b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/android-shim/README.md @@ -0,0 +1,7 @@ +# Android CMake helper shim + +This directory contains only `SoMerging-utils.cmake`, included by React +Native's `jsinspector-modern` CMake files through `REACT_ANDROID_DIR`. The +helper defines `target_merge_so()`, which is intentionally a no-op on GPUI's +non-Android desktop builds. No Android JNI, Java, or runtime implementation is +compiled or shipped from this directory. diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/android-shim/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/android-shim/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake new file mode 100644 index 0000000000..44c93592df --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/android-shim/src/main/jni/first-party/jni-lib-merge/SoMerging-utils.cmake @@ -0,0 +1,27 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +# This function should be invoked if the requested library is going to be merged +# with libreactnative.so as such: +# +# target_merge_so(react_newarchdefaults) +# +# This function will take care of forcefully including the jni_lib_merge.h header that takes +# care of redefining the JNI_OnLoad function to JNI_OnLoad_Weak. +function(target_merge_so target_name) + if(NOT ANDROID) + return() + endif() + + target_compile_options(${target_name} + PRIVATE + -DORIGINAL_SONAME=\"lib${target_name}.so\" + -include ${REACT_ANDROID_DIR}/src/main/jni/first-party/jni-lib-merge/jni_lib_merge.h + ) + target_link_options(${target_name} PRIVATE -Wl,--defsym=JNI_OnLoad=JNI_OnLoad_Weak) +endfunction() diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/context_menu_view/GpuiContextMenuViewProps.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/context_menu_view/GpuiContextMenuViewProps.cpp new file mode 100644 index 0000000000..559eec86d0 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/context_menu_view/GpuiContextMenuViewProps.cpp @@ -0,0 +1,42 @@ +// Props for the native context-menu view, read by the Rust renderer as JSON. +// +// Registered the way core's other built-ins are (see +// `window_control/GpuiWindowControlProps.cpp`): an installer listed in +// `cpp/CMakeLists.txt`, called once by the generated +// `rn_gpui_install_modules()`. +// +// `items` goes out as-is. It is the same `MenuItemSpec[]` shape +// `contextMenuShow` takes, so the Rust side parses both with one function and a +// menu cannot mean one thing from JS and another from this component. + +#include +#include + +#include "GpuiModules.h" +#include "react/renderer/components/desktopcontextmenuview/DesktopContextMenuView.h" + +namespace rngpui { +namespace { + +std::string contextMenuViewPropsJson( + const std::string& /*componentName*/, + const facebook::react::ShadowView& view) { + if (!view.props) { + return std::string("{}"); + } + const auto& props = *std::static_pointer_cast< + const facebook::react::DesktopContextMenuViewProps>(view.props); + folly::dynamic object = folly::dynamic::object; + object["items"] = props.items; + object["presentation"] = props.presentation; + object["disabled"] = props.disabled; + return folly::toJson(object); +} + +} // namespace + +void rn_gpui_install_context_menu_view_module() { + registerComponentProps("DesktopContextMenuView", contextMenuViewPropsJson); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/image/GpuiImageProps.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/image/GpuiImageProps.cpp new file mode 100644 index 0000000000..c457a6d9c6 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/image/GpuiImageProps.cpp @@ -0,0 +1,47 @@ + +#include + +#include +#include + +#include "GpuiModules.h" + +namespace rngpui { +namespace { + +std::string imagePropsJson( + const std::string& /*componentName*/, + const facebook::react::ShadowView& view) { + if (!view.props) { + return std::string("{}"); + } + const auto& props = + *std::static_pointer_cast(view.props); + folly::dynamic object = folly::dynamic::object; + if (!props.sources.empty()) { + object["uri"] = props.sources[0].uri; + } + switch (props.resizeMode) { + case facebook::react::ImageResizeMode::Contain: + object["resizeMode"] = "contain"; + break; + case facebook::react::ImageResizeMode::Cover: + object["resizeMode"] = "cover"; + break; + case facebook::react::ImageResizeMode::Center: + object["resizeMode"] = "center"; + break; + default: + object["resizeMode"] = "stretch"; + break; + } + return folly::toJson(object); +} + +} // namespace + +void rn_gpui_install_image_module() { + registerComponentProps("Image", imagePropsJson); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/modal/GpuiModalProps.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/modal/GpuiModalProps.cpp new file mode 100644 index 0000000000..fd3e270f32 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/modal/GpuiModalProps.cpp @@ -0,0 +1,29 @@ + +#include + +#include +#include + +#include "GpuiModules.h" + +namespace rngpui { +namespace { + +std::string modalPropsJson( + const std::string& /*componentName*/, + const facebook::react::ShadowView& view) { + const auto props = + std::dynamic_pointer_cast( + view.props); + folly::dynamic object = folly::dynamic::object; + object["visible"] = props ? props->visible : true; + return folly::toJson(object); +} + +} // namespace + +void rn_gpui_install_modal_module() { + registerComponentProps("ModalHostView", modalPropsJson); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/scrollview/GpuiScrollViewProps.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/scrollview/GpuiScrollViewProps.cpp new file mode 100644 index 0000000000..82ff9aee01 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/scrollview/GpuiScrollViewProps.cpp @@ -0,0 +1,51 @@ + +#include +#include +#include + +#include +#include + +#include "GpuiModules.h" + +namespace rngpui { +namespace { + +std::string scrollViewPropsJson( + const std::string& /*componentName*/, + const facebook::react::ShadowView& view) { + folly::dynamic object = folly::dynamic::object; + bool horizontal = false; + bool enabled = true; + bool indicator = true; + if (auto props = + std::dynamic_pointer_cast( + view.props)) { + horizontal = props->horizontal; + enabled = props->scrollEnabled; + indicator = props->horizontal ? props->showsHorizontalScrollIndicator + : props->showsVerticalScrollIndicator; + } + object["horizontal"] = horizontal; + object["enabled"] = enabled; + object["indicator"] = indicator; + if (auto state = + std::dynamic_pointer_cast>(view.state)) { + const auto& data = state->getData(); + const auto content = data.getContentSize(); + object["contentWidth"] = content.width; + object["contentHeight"] = content.height; + object["offsetX"] = data.contentOffset.x; + object["offsetY"] = data.contentOffset.y; + } + return folly::toJson(object); +} + +} // namespace + +void rn_gpui_install_scroll_view_module() { + registerComponentProps("ScrollView", scrollViewPropsJson); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/shortcut_scope/GpuiShortcutScopeProps.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/shortcut_scope/GpuiShortcutScopeProps.cpp new file mode 100644 index 0000000000..0e665a3fea --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/shortcut_scope/GpuiShortcutScopeProps.cpp @@ -0,0 +1,41 @@ +// Props for the shortcut scope, read by the Rust renderer as JSON. +// +// Registered the way core's other built-ins are (see +// `window_control/GpuiWindowControlProps.cpp`): an installer listed in +// `cpp/CMakeLists.txt`, called once by the generated +// `rn_gpui_install_modules()`. +// +// The props are one string. Everything the component means is native — the +// `name` becomes a keymap context and GPUI resolves the precedence — so there +// is nothing else to send. + +#include +#include + +#include "GpuiModules.h" +#include "react/renderer/components/shortcutscope/ShortcutScope.h" + +namespace rngpui { +namespace { + +std::string shortcutScopePropsJson( + const std::string& /*componentName*/, + const facebook::react::ShadowView& view) { + if (!view.props) { + return std::string("{}"); + } + const auto& props = + *std::static_pointer_cast( + view.props); + folly::dynamic object = folly::dynamic::object; + object["name"] = props.name; + return folly::toJson(object); +} + +} // namespace + +void rn_gpui_install_shortcut_scope_module() { + registerComponentProps("DesktopShortcutScope", shortcutScopePropsJson); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/textinput/GpuiTextInputProps.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/textinput/GpuiTextInputProps.cpp new file mode 100644 index 0000000000..4381023623 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/textinput/GpuiTextInputProps.cpp @@ -0,0 +1,68 @@ + +#include +#include +#include + +#include +#include +#include +#include + +#include "GpuiModules.h" + +namespace rngpui { +namespace { + +uint32_t argb(const facebook::react::SharedColor& color) { + if (!color) { + return 0; + } + auto components = facebook::react::colorComponentsFromColor(color); + auto to8 = [](float value) { + return static_cast(std::lround(value * 255.f)) & 0xFF; + }; + return (to8(components.alpha) << 24) | (to8(components.red) << 16) | + (to8(components.green) << 8) | to8(components.blue); +} + +std::string textInputPropsJson( + const std::string& /*componentName*/, + const facebook::react::ShadowView& view) { + folly::dynamic object = folly::dynamic::object; + if (auto props = + std::dynamic_pointer_cast( + view.props)) { + object["text"] = props->text; + object["placeholder"] = props->placeholder; + object["selectionColor"] = static_cast(argb(props->selectionColor)); + object["multiline"] = props->multiline; + object["editable"] = props->editable && !props->readOnly; + object["secure"] = props->traits.secureTextEntry; + object["eventCount"] = props->mostRecentEventCount; + bool selection_set = props->selection.has_value(); + object["selectionSet"] = selection_set; + if (selection_set) { + object["selectionStart"] = props->selection->start; + object["selectionEnd"] = props->selection->end; + } else { + int32_t cursor = 0; + for (const auto byte : props->text) { + const auto value = static_cast(byte); + if ((value & 0xC0) != 0x80) { + cursor += value >= 0xF0 ? 2 : 1; + } + } + object["selectionStart"] = cursor; + object["selectionEnd"] = cursor; + } + } + return folly::toJson(object); +} + +} // namespace + +void rn_gpui_install_text_input_module() { + registerComponentProps("TextInput", textInputPropsJson); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/window_control/GpuiWindowControlProps.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/window_control/GpuiWindowControlProps.cpp new file mode 100644 index 0000000000..883cfb02f0 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/builtins/window_control/GpuiWindowControlProps.cpp @@ -0,0 +1,47 @@ +// Props for the caption button, read by the Rust renderer as JSON. +// +// Registered the way core's other built-ins are (see +// `image/GpuiImageProps.cpp`): an installer listed in `cpp/CMakeLists.txt`, +// called once by the generated `rn_gpui_install_modules()`. + +#include +#include + +#include "GpuiModules.h" +#include "react/renderer/components/windowcontrol/WindowControl.h" + +namespace rngpui { +namespace { + +std::string windowControlPropsJson( + const std::string& /*componentName*/, + const facebook::react::ShadowView& view) { + if (!view.props) { + return std::string("{}"); + } + const auto& props = + *std::static_pointer_cast( + view.props); + folly::dynamic object = folly::dynamic::object; + switch (props.action) { + case facebook::react::WindowControlAction::Minimize: + object["action"] = "minimize"; + break; + case facebook::react::WindowControlAction::Maximize: + object["action"] = "maximize"; + break; + default: + object["action"] = "close"; + break; + } + object["disabled"] = props.disabled; + return folly::toJson(object); +} + +} // namespace + +void rn_gpui_install_window_control_module() { + registerComponentProps("DesktopWindowControl", windowControlPropsJson); +} + +} // namespace rngpui diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/DownloadNativeDeps.cmake b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/DownloadNativeDeps.cmake new file mode 100644 index 0000000000..ab5fe53d80 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/DownloadNativeDeps.cmake @@ -0,0 +1,333 @@ +set(RN_GPUI_NATIVE_DEPS_CACHE "${CMAKE_BINARY_DIR}/native-deps") +set(RN_GPUI_NATIVE_DEPS_DOWNLOADS "${RN_GPUI_NATIVE_DEPS_CACHE}/downloads") +set(RN_GPUI_NATIVE_DEPS_SOURCES "${RN_GPUI_NATIVE_DEPS_CACHE}/sources") +set(RN_GPUI_NATIVE_DEPS_PREPARED "${RN_GPUI_NATIVE_DEPS_CACHE}/third-party-ndk") +file(MAKE_DIRECTORY "${RN_GPUI_NATIVE_DEPS_DOWNLOADS}" "${RN_GPUI_NATIVE_DEPS_SOURCES}") + +function(rn_gpui_download_extract name archive_name url out_source) + set(_archive "${RN_GPUI_NATIVE_DEPS_DOWNLOADS}/${archive_name}") + set(_source "${RN_GPUI_NATIVE_DEPS_SOURCES}/${name}") + if(NOT EXISTS "${_archive}") + message(STATUS "Downloading ${archive_name} into ${RN_GPUI_NATIVE_DEPS_DOWNLOADS}") + set(_download_succeeded FALSE) + foreach(_attempt RANGE 1 5) + set(_partial_archive "${_archive}.part") + file(REMOVE "${_partial_archive}") + # No overall TIMEOUT: Boost is 145 MB, and a cap on the whole transfer + # fails every attempt on a slow link however steadily it downloads. + file(DOWNLOAD "${url}" "${_partial_archive}" SHOW_PROGRESS TLS_VERIFY ON + INACTIVITY_TIMEOUT 45 STATUS _download_status) + list(GET _download_status 0 _download_code) + if(_download_code EQUAL 0) + file(RENAME "${_partial_archive}" "${_archive}") + set(_download_succeeded TRUE) + break() + endif() + list(GET _download_status 1 _download_message) + file(REMOVE "${_partial_archive}") + if(_attempt LESS 5) + math(EXPR _retry_delay "${_attempt} * 2") + message(STATUS "Download attempt ${_attempt}/5 failed (${_download_message}); retrying in ${_retry_delay}s") + execute_process(COMMAND "${CMAKE_COMMAND}" -E sleep "${_retry_delay}") + endif() + endforeach() + if(NOT _download_succeeded) + message(FATAL_ERROR "Failed to download ${url} after 5 attempts: ${_download_message}") + endif() + endif() + if(NOT EXISTS "${_source}/.extracted") + file(REMOVE_RECURSE "${_source}") + file(MAKE_DIRECTORY "${_source}") + file(ARCHIVE_EXTRACT INPUT "${_archive}" DESTINATION "${_source}") + file(WRITE "${_source}/.extracted" "${url}\n") + endif() + set(${out_source} "${_source}" PARENT_SCOPE) +endfunction() + +# Keep these versions and source URLs aligned with node_modules/react-native/ +# gradle/libs.versions.toml and ReactAndroid/build.gradle.kts. +rn_gpui_download_extract(boost "boost_1_83_0.tar.gz" + "https://archives.boost.io/release/1.83.0/source/boost_1_83_0.tar.gz" RN_GPUI_BOOST_ARCHIVE_DIR) +rn_gpui_download_extract(double-conversion "double-conversion-1.1.6.tar.gz" + "https://github.com/google/double-conversion/archive/v1.1.6.tar.gz" RN_GPUI_DOUBLE_CONVERSION_ARCHIVE_DIR) +rn_gpui_download_extract(fast_float "fast_float-8.0.0.tar.gz" + "https://github.com/fastfloat/fast_float/archive/v8.0.0.tar.gz" RN_GPUI_FAST_FLOAT_ARCHIVE_DIR) +rn_gpui_download_extract(fmt "fmt-12.1.0.tar.gz" + "https://github.com/fmtlib/fmt/archive/12.1.0.tar.gz" RN_GPUI_FMT_ARCHIVE_DIR) +rn_gpui_download_extract(folly "folly-2024.11.18.00.tar.gz" + "https://github.com/facebook/folly/archive/v2024.11.18.00.tar.gz" RN_GPUI_FOLLY_ARCHIVE_DIR) +rn_gpui_download_extract(glog "glog-0.3.5.tar.gz" + "https://github.com/google/glog/archive/v0.3.5.tar.gz" RN_GPUI_GLOG_ARCHIVE_DIR) +rn_gpui_download_extract(nlohmann_json "json-3.11.2.tar.gz" + "https://github.com/nlohmann/json/archive/v3.11.2.tar.gz" RN_GPUI_NLOHMANN_JSON_ARCHIVE_DIR) +rn_gpui_download_extract(openssl "openssl-3.3.2.tar.gz" + "https://www.openssl.org/source/openssl-3.3.2.tar.gz" RN_GPUI_OPENSSL_ARCHIVE_DIR) +rn_gpui_download_extract(curl "curl-8.11.1.tar.gz" + "https://curl.se/download/curl-8.11.1.tar.gz" RN_GPUI_CURL_ARCHIVE_DIR) + +# OpenSSL's upstream project uses Configure + Make rather than CMake. Build a +# static copy into the same per-target cache before CMake discovers it; this +# keeps CURL and ReactCxxPlatform off host OpenSSL installations. +file(GLOB _openssl_roots LIST_DIRECTORIES TRUE "${RN_GPUI_OPENSSL_ARCHIVE_DIR}/openssl-3.3.2*") +list(GET _openssl_roots 0 RN_GPUI_OPENSSL_SOURCE_DIR) +set(RN_GPUI_OPENSSL_INSTALL "${RN_GPUI_NATIVE_DEPS_CACHE}/install/openssl") +if(WIN32) + set(_openssl_crypto_library "${RN_GPUI_OPENSSL_INSTALL}/lib/libcrypto.lib") +else() + set(_openssl_crypto_library "${RN_GPUI_OPENSSL_INSTALL}/lib/libcrypto.a") +endif() +# Prebuilt mode: the archive carries OpenSSL's libraries and generated headers, +# so only download and extract here (the blocks below are source-mode only). +if(NOT RN_GPUI_PREBUILT_DIR AND NOT EXISTS "${_openssl_crypto_library}") + # OpenSSL's Configure needs a native Windows Perl. Git for Windows ships an + # MSYS Perl without Locale::Maketext::Simple and puts it ahead on PATH in Git + # Bash, so look in Strawberry Perl's default location first and check the + # module before Configure fails with a far less readable error. + find_program(RN_GPUI_PERL_EXECUTABLE perl + HINTS "C:/Strawberry/perl/bin" REQUIRED) + if(WIN32) + execute_process( + COMMAND "${RN_GPUI_PERL_EXECUTABLE}" -MLocale::Maketext::Simple -e 1 + RESULT_VARIABLE _rn_gpui_perl_check + OUTPUT_QUIET ERROR_QUIET) + if(NOT _rn_gpui_perl_check EQUAL 0) + message(FATAL_ERROR + "${RN_GPUI_PERL_EXECUTABLE} cannot build OpenSSL (it lacks " + "Locale::Maketext::Simple; Git for Windows' Perl does). Install Strawberry " + "Perl, put it ahead of Git on PATH, or pass -DRN_GPUI_PERL_EXECUTABLE=.") + endif() + find_program(RN_GPUI_MAKE_EXECUTABLE nmake REQUIRED) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + set(_openssl_target VC-WIN64A) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|arm64|aarch64") + set(_openssl_target VC-WIN64-ARM) + else() + message(FATAL_ERROR "No OpenSSL source-build target for Windows/${CMAKE_SYSTEM_PROCESSOR}") + endif() + else() + find_program(RN_GPUI_MAKE_EXECUTABLE make REQUIRED) + endif() + if(WIN32) + # The VS developer environment must be active so OpenSSL uses the same + # MSVC toolchain/runtime as React Native and the GPUI host. + elseif(APPLE AND CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64") + set(_openssl_target darwin64-arm64-cc) + elseif(APPLE) + set(_openssl_target darwin64-x86_64-cc) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + set(_openssl_target linux-x86_64) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(_openssl_target linux-aarch64) + else() + message(FATAL_ERROR "No OpenSSL source-build target for ${CMAKE_SYSTEM_NAME}/${CMAKE_SYSTEM_PROCESSOR}") + endif() + # no-apps: GPUI links only libssl and libcrypto; the openssl CLI in apps/ + # is not needed. The Xcode toolchain cc has no default sysroot, so + # is missing from every OpenSSL compile unless SDKROOT names + # the SDK; OpenSSL's Makefile does not pass -isysroot itself. + set(_openssl_env ${CMAKE_COMMAND} -E env) + if(APPLE) + execute_process(COMMAND xcrun --sdk macosx --show-sdk-path + OUTPUT_VARIABLE _openssl_sdk OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _openssl_sdk_status) + if(NOT _openssl_sdk_status EQUAL 0 OR _openssl_sdk STREQUAL "") + message(FATAL_ERROR "xcrun could not locate the macOS SDK for OpenSSL") + endif() + set(_openssl_env ${CMAKE_COMMAND} -E env "SDKROOT=${_openssl_sdk}") + endif() + execute_process( + COMMAND ${_openssl_env} "${RN_GPUI_PERL_EXECUTABLE}" ./Configure "${_openssl_target}" no-shared no-tests no-apps + "--prefix=${RN_GPUI_OPENSSL_INSTALL}" --libdir=lib + WORKING_DIRECTORY "${RN_GPUI_OPENSSL_SOURCE_DIR}" + RESULT_VARIABLE _openssl_configure_status) + if(NOT _openssl_configure_status EQUAL 0) + message(FATAL_ERROR "OpenSSL Configure failed for ${_openssl_target}") + endif() + if(WIN32) + execute_process( + COMMAND "${RN_GPUI_MAKE_EXECUTABLE}" build_libs + WORKING_DIRECTORY "${RN_GPUI_OPENSSL_SOURCE_DIR}" + RESULT_VARIABLE _openssl_build_status) + else() + execute_process( + COMMAND ${_openssl_env} "${RN_GPUI_MAKE_EXECUTABLE}" -j2 build_libs + WORKING_DIRECTORY "${RN_GPUI_OPENSSL_SOURCE_DIR}" + RESULT_VARIABLE _openssl_build_status) + endif() + if(NOT _openssl_build_status EQUAL 0) + message(FATAL_ERROR "Building cached OpenSSL source failed") + endif() + execute_process( + COMMAND ${_openssl_env} "${RN_GPUI_MAKE_EXECUTABLE}" install_dev + WORKING_DIRECTORY "${RN_GPUI_OPENSSL_SOURCE_DIR}" + RESULT_VARIABLE _openssl_install_status) + if(NOT _openssl_install_status EQUAL 0) + message(FATAL_ERROR "Installing cached OpenSSL into the CMake build cache failed") + endif() +endif() +list(PREPEND CMAKE_PREFIX_PATH "${RN_GPUI_OPENSSL_INSTALL}") +set(OPENSSL_ROOT_DIR "${RN_GPUI_OPENSSL_INSTALL}" CACHE PATH "GPUI build-cache OpenSSL" FORCE) +set(OPENSSL_USE_STATIC_LIBS TRUE) + +file(GLOB _curl_roots LIST_DIRECTORIES TRUE "${RN_GPUI_CURL_ARCHIVE_DIR}/curl-8.11.1*") +list(GET _curl_roots 0 RN_GPUI_CURL_SOURCE_DIR) + +file(MAKE_DIRECTORY "${RN_GPUI_NATIVE_DEPS_PREPARED}") +set(RN_GPUI_NATIVE_DEPS_BUILD "${RN_GPUI_NATIVE_DEPS_CACHE}/build") +file(MAKE_DIRECTORY "${RN_GPUI_NATIVE_DEPS_BUILD}") + +# ReactAndroid's Gradle prepare tasks strip unused files and retain only what +# its CMake dependency targets compile. Use the same CMake recipes from RN. +set(_rn_third_party "${REACT_ANDROID_SOURCE_DIR}/src/main/jni/third-party") + +set(_dst "${RN_GPUI_NATIVE_DEPS_PREPARED}/boost") +file(REMOVE_RECURSE "${_dst}") +file(MAKE_DIRECTORY "${_dst}") +file(GLOB _boost_root LIST_DIRECTORIES TRUE "${RN_GPUI_BOOST_ARCHIVE_DIR}/boost_1_83_0*") +list(GET _boost_root 0 _boost_root) +file(COPY "${_boost_root}/boost" DESTINATION "${_dst}/boost_1_83_0") + +set(_dst "${RN_GPUI_NATIVE_DEPS_PREPARED}/double-conversion") +file(REMOVE_RECURSE "${_dst}") +file(MAKE_DIRECTORY "${_dst}") +file(GLOB _double_root LIST_DIRECTORIES TRUE "${RN_GPUI_DOUBLE_CONVERSION_ARCHIVE_DIR}/double-conversion-1.1.6*") +list(GET _double_root 0 _double_root) +file(COPY "${_double_root}/src/" DESTINATION "${_dst}/double-conversion") +configure_file("${_rn_third_party}/double-conversion/CMakeLists.txt" "${_dst}/CMakeLists.txt" COPYONLY) +# double-conversion 1.1.6 predates MSVC ARM64 and stops on `#error` there; ARM64 +# has correct double operations, as the `__aarch64__` branch already says. +set(_double_utils "${_dst}/double-conversion/utils.h") +file(READ "${_double_utils}" _double_utils_text) +string(REPLACE "#if defined(_M_X64) || defined(__x86_64__)" + "#if defined(_M_X64) || defined(_M_ARM64) || defined(__x86_64__)" + _double_utils_patched "${_double_utils_text}") +if(_double_utils_patched STREQUAL _double_utils_text) + message(FATAL_ERROR "double-conversion changed ${_double_utils}; update the ARM64 check") +endif() +file(WRITE "${_double_utils}" "${_double_utils_patched}") + +set(_dst "${RN_GPUI_NATIVE_DEPS_PREPARED}/fast_float") +file(REMOVE_RECURSE "${_dst}") +file(MAKE_DIRECTORY "${_dst}") +file(GLOB _fast_float_root LIST_DIRECTORIES TRUE "${RN_GPUI_FAST_FLOAT_ARCHIVE_DIR}/fast_float-8.0.0*") +list(GET _fast_float_root 0 _fast_float_root) +file(COPY "${_fast_float_root}/include" DESTINATION "${_dst}") +configure_file("${_rn_third_party}/fast_float/CMakeLists.txt" "${_dst}/CMakeLists.txt" COPYONLY) + +set(_dst "${RN_GPUI_NATIVE_DEPS_PREPARED}/fmt") +file(REMOVE_RECURSE "${_dst}") +file(MAKE_DIRECTORY "${_dst}") +file(GLOB _fmt_root LIST_DIRECTORIES TRUE "${RN_GPUI_FMT_ARCHIVE_DIR}/fmt-12.1.0*") +list(GET _fmt_root 0 _fmt_root) +file(COPY "${_fmt_root}/src" "${_fmt_root}/include" DESTINATION "${_dst}") +configure_file("${_rn_third_party}/fmt/CMakeLists.txt" "${_dst}/CMakeLists.txt" COPYONLY) + +set(_dst "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly") +file(REMOVE_RECURSE "${_dst}") +file(MAKE_DIRECTORY "${_dst}") +file(GLOB _folly_root LIST_DIRECTORIES TRUE "${RN_GPUI_FOLLY_ARCHIVE_DIR}/folly-2024.11.18.00*") +list(GET _folly_root 0 _folly_root) +file(COPY "${_folly_root}/folly" DESTINATION "${_dst}") +file(READ "${_rn_third_party}/folly/CMakeLists.txt" _folly_cmake) +# ReactAndroid targets libc++; GPUI uses the host C++ standard library. +string(REPLACE " -DFOLLY_USE_LIBCPP=1\n" "" _folly_cmake "${_folly_cmake}") +if(WIN32) + # The rest of ReactAndroid's folly flags describe a POSIX/Android host: + # MSVC has no pthreads, recvmmsg, XSI strerror_r or clock_gettime, and folly + # selects its Windows implementations when those are not defined. + foreach(_folly_flag IN ITEMS + "-DFOLLY_HAVE_PTHREAD=1" + "-DFOLLY_HAVE_RECVMMSG=1" + "-DFOLLY_HAVE_XSI_STRERROR_R=1" + "-DFOLLY_HAVE_CLOCK_GETTIME=1" + "-DFOLLY_MOBILE=1") + string(REPLACE "${_folly_flag}\n" "\n" _folly_cmake "${_folly_cmake}") + endforeach() +endif() +file(WRITE "${_dst}/CMakeLists.txt" "${_folly_cmake}") + +set(_dst "${RN_GPUI_NATIVE_DEPS_PREPARED}/glog") +file(REMOVE_RECURSE "${_dst}") +file(MAKE_DIRECTORY "${_dst}/glog-0.3.5" "${_dst}/exported/glog") +file(GLOB _glog_root LIST_DIRECTORIES TRUE "${RN_GPUI_GLOG_ARCHIVE_DIR}/glog-0.3.5*") +list(GET _glog_root 0 _glog_root) +file(COPY "${_glog_root}/src" DESTINATION "${_dst}/glog-0.3.5") +configure_file("${_rn_third_party}/glog/CMakeLists.txt" "${_dst}/CMakeLists.txt" COPYONLY) +configure_file("${_rn_third_party}/glog/config.h" "${_dst}/config.h" COPYONLY) +file(GLOB_RECURSE _glog_templates "${_dst}/glog-0.3.5/src/*.h.in") +set(_glog_token_names + ac_cv_have_unistd_h ac_cv_have_stdint_h ac_cv_have_systypes_h ac_cv_have_inttypes_h + ac_cv_have_libgflags ac_google_start_namespace ac_cv_have_uint16_t ac_cv_have_u_int16_t + ac_cv_have___uint16 ac_google_end_namespace ac_cv_have___builtin_expect ac_google_namespace + ac_cv___attribute___noinline ac_cv___attribute___noreturn ac_cv___attribute___printf_4_5 + ac_cv_cxx_using_operator) +if(WIN32) + # MSVC has no , no __builtin_expect and no GNU __attribute__. The + # attribute tokens are commented out rather than emptied: CMake lists cannot + # carry empty elements reliably, and a comment keeps the declarations valid. + set(_glog_have_unistd_h 0) + set(_glog_have_builtin_expect 0) + set(_glog_attribute_noinline "/* noinline */") + set(_glog_attribute_noreturn "/* noreturn */") + set(_glog_attribute_printf "/* printf format attribute */") +else() + set(_glog_have_unistd_h 1) + set(_glog_have_builtin_expect 1) + set(_glog_attribute_noinline "__attribute__ ((noinline))") + set(_glog_attribute_noreturn "__attribute__ ((noreturn))") + set(_glog_attribute_printf "__attribute__((__format__ (__printf__, 4, 5)))") +endif() +set(_glog_token_values + ${_glog_have_unistd_h} 1 1 1 0 "namespace google {" 1 1 0 "}" ${_glog_have_builtin_expect} google + "${_glog_attribute_noinline}" "${_glog_attribute_noreturn}" + "${_glog_attribute_printf}" 1) +foreach(_template IN LISTS _glog_templates) + file(READ "${_template}" _glog_content) + list(LENGTH _glog_token_names _glog_token_count) + math(EXPR _glog_token_last "${_glog_token_count} - 1") + foreach(_index RANGE 0 ${_glog_token_last}) + list(GET _glog_token_names ${_index} _token_name) + list(GET _glog_token_values ${_index} _token_value) + string(REPLACE "@${_token_name}@" "${_token_value}" _glog_content "${_glog_content}") + endforeach() + string(REGEX REPLACE "\\.h\\.in$" ".h" _generated_header "${_template}") + file(WRITE "${_generated_header}" "${_glog_content}") +endforeach() +# config.h.in is an Autoconf template with #undef directives, not @name@ +# substitutions. ReactAndroid ships the configured platform header separately; +# place it at both include locations because glog's sources search src/ first. +if(WIN32) + # ReactAndroid's config.h is generated for Android/Linux and describes + # headers and builtins MSVC does not have, so Windows gets its own. + configure_file("${CMAKE_CURRENT_LIST_DIR}/glog-windows-config.h" + "${_dst}/glog-0.3.5/src/config.h" COPYONLY) + configure_file("${CMAKE_CURRENT_LIST_DIR}/glog-windows-config.h" + "${_dst}/config.h" COPYONLY) + # defines an ERROR macro, and glog refuses to compile when it is + # set. glog's escape hatch (GLOG_NO_ABBREVIATED_SEVERITIES) would force every + # ReactCommon call site onto the GLOG_ERROR spelling, so drop the macro + # instead of the severities. + file(GLOB_RECURSE _glog_severity_headers "${_dst}/glog-0.3.5/src/*log_severity.h") + foreach(_glog_severity IN LISTS _glog_severity_headers) + file(READ "${_glog_severity}" _glog_severity_content) + string(REPLACE "# error ERROR macro is defined." "# undef ERROR /* patched for Windows */ //" + _glog_severity_content "${_glog_severity_content}") + file(WRITE "${_glog_severity}" "${_glog_severity_content}") + endforeach() +else() + configure_file("${_rn_third_party}/glog/config.h" "${_dst}/glog-0.3.5/src/config.h" COPYONLY) +endif() +foreach(_header IN ITEMS stl_logging.h logging.h raw_logging.h vlog_is_on.h log_severity.h) + if(EXISTS "${_dst}/glog-0.3.5/src/${_header}") + configure_file("${_dst}/glog-0.3.5/src/${_header}" "${_dst}/exported/glog/${_header}" COPYONLY) + elseif(EXISTS "${_dst}/glog-0.3.5/src/glog/${_header}") + configure_file("${_dst}/glog-0.3.5/src/glog/${_header}" "${_dst}/exported/glog/${_header}" COPYONLY) + endif() +endforeach() + +set(_dst "${RN_GPUI_NATIVE_DEPS_PREPARED}/nlohmann_json") +file(REMOVE_RECURSE "${_dst}") +file(MAKE_DIRECTORY "${_dst}") +file(GLOB _json_root LIST_DIRECTORIES TRUE "${RN_GPUI_NLOHMANN_JSON_ARCHIVE_DIR}/json-3.11.2*") +list(GET _json_root 0 _json_root) +file(COPY "${_json_root}/include" DESTINATION "${_dst}") diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/NodeModules.cmake b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/NodeModules.cmake new file mode 100644 index 0000000000..e9fcf73ef2 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/NodeModules.cmake @@ -0,0 +1,28 @@ +# Finds an installed package without assuming a repository layout. +# +# Every package lives at `node_modules/` in an app, but this repository +# keeps its own packages at different depths (`packages/core`, `modules/fonts`), +# so counting parent directories can only be right in one of the two. The walk +# starts at the project root and goes up, which covers a package hoisted to an +# ancestor `node_modules` (npm workspaces) as well as one sitting beside the app +# (pnpm, a nested install). +function(rn_gpui_find_node_module package out_var) + if(NOT RN_GPUI_PROJECT_ROOT) + message(FATAL_ERROR + "rn_gpui_find_node_module(${package}) needs RN_GPUI_PROJECT_ROOT, which the platform's build.rs sets") + endif() + set(_rn_gpui_search "${RN_GPUI_PROJECT_ROOT}") + while(_rn_gpui_search) + if(EXISTS "${_rn_gpui_search}/node_modules/${package}/package.json") + set(${out_var} "${_rn_gpui_search}/node_modules/${package}" PARENT_SCOPE) + return() + endif() + get_filename_component(_rn_gpui_parent "${_rn_gpui_search}" DIRECTORY) + if(_rn_gpui_parent STREQUAL _rn_gpui_search) + break() + endif() + set(_rn_gpui_search "${_rn_gpui_parent}") + endwhile() + message(FATAL_ERROR + "${package} was not found in any node_modules at or above ${RN_GPUI_PROJECT_ROOT}") +endfunction() diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/glog-windows-config.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/glog-windows-config.h new file mode 100644 index 0000000000..d8f575d130 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/glog-windows-config.h @@ -0,0 +1,33 @@ +/* glog configuration for MSVC on Windows. + * + * ReactAndroid ships an Autoconf config.h generated on Android/Linux, and + * glog 0.3.5 predates MSVC support in its own build system, so the Windows + * answers are provided here instead of reusing those POSIX results. Anything + * not defined here is deliberately absent: no , , + * or , no __sync builtins, no GNU __attribute__, + * no __builtin_expect, no execinfo/glob/pwd/sigaction/sigaltstack/syslog and + * no pread or pthreads. + */ +/* No HAVE_FCNTL: MSVC's lacks F_SETFD/FD_CLOEXEC, and glog's + * fopen fallback is the Windows path. */ +#define HAVE_INTTYPES_H 1 +#define HAVE_SNPRINTF 1 +#define HAVE_STDINT_H 1 +#define HAVE_STRING_H 1 +#define HAVE_SYS_STAT_H 1 +#define HAVE_SYS_TYPES_H 1 +#define HAVE_TIME_H 1 +#define HAVE_USING_OPERATOR 1 + +/* glog's headers wrap their declarations in these namespace macros. */ +#define _START_GOOGLE_NAMESPACE_ namespace google { +#define _END_GOOGLE_NAMESPACE_ } + +#define STL_NAMESPACE std + +/* ReactAndroid's android-* builds are 32-bit; this one is 64-bit. */ +#define SIZEOF_VOID_P 8 + +#define PACKAGE "glog" +#define PACKAGE_VERSION "0.3.5" +#define VERSION "0.3.5" diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/modules/PrebuiltInstall.cmake b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/modules/PrebuiltInstall.cmake new file mode 100644 index 0000000000..b9fd7b279b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/modules/PrebuiltInstall.cmake @@ -0,0 +1,400 @@ +include(GNUInstallDirs) + +set(RN_GPUI_PREBUILT_CONFIG_NAME "react-native-gpui-prebuilt") +set(_rn_gpui_prebuilt_gen "${CMAKE_BINARY_DIR}/prebuilt") + +# --- classify the link closure ------------------------------------------------- +# Skipped: app-side and per-app targets (rn_gpui_app_turbo_modules, +# rn_gpui_modules, rn_gpui_autolink_*). +set(_rn_gpui_built "") # real archives/dylibs we built (names resolved) +set(_rn_gpui_imported "") # imported archives/dylibs, names as in the closure +set(_rn_gpui_shadow "") # OBJECT/INTERFACE names +set(_rn_gpui_order "") # ordered entries that real/imported targets need +foreach(_rn_gpui_entry IN LISTS RN_GPUI_LINK_TARGETS) + if(_rn_gpui_entry STREQUAL "rn_gpui_app_turbo_modules" + OR _rn_gpui_entry STREQUAL "rn_gpui_modules" + OR _rn_gpui_entry MATCHES "^rn_gpui_autolink_") + continue() + endif() + get_target_property(_rn_gpui_type "${_rn_gpui_entry}" TYPE) + get_target_property(_rn_gpui_aliased "${_rn_gpui_entry}" ALIASED_TARGET) + if(_rn_gpui_aliased) + set(_rn_gpui_real "${_rn_gpui_aliased}") + else() + set(_rn_gpui_real "${_rn_gpui_entry}") + endif() + if(_rn_gpui_type MATCHES "^(OBJECT|INTERFACE)_LIBRARY$") + list(APPEND _rn_gpui_shadow "${_rn_gpui_entry}") + elseif(_rn_gpui_type MATCHES "^(STATIC|SHARED)_LIBRARY$" OR _rn_gpui_type STREQUAL "UNKNOWN_LIBRARY") + get_target_property(_rn_gpui_imported_flag "${_rn_gpui_real}" IMPORTED) + if(_rn_gpui_imported_flag) + list(APPEND _rn_gpui_imported "${_rn_gpui_real}") + else() + list(APPEND _rn_gpui_built "${_rn_gpui_real}") + endif() + list(APPEND _rn_gpui_order "${_rn_gpui_entry}") + endif() +endforeach() +list(REMOVE_DUPLICATES _rn_gpui_built) +list(REMOVE_DUPLICATES _rn_gpui_imported) + +install(TARGETS ${_rn_gpui_built} + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin) + +# Imported archives (OpenSSL and the third-party libraries) are copied from the +# path configure found. An engine's own shared library is not part of the +# archive: the installed engine package ships it. +set(_rn_gpui_imported_paths "") +foreach(_rn_gpui_imp IN LISTS _rn_gpui_imported) + get_target_property(_rn_gpui_loc "${_rn_gpui_imp}" IMPORTED_LOCATION) + if(_rn_gpui_loc AND EXISTS "${_rn_gpui_loc}") + list(APPEND _rn_gpui_imported_paths "${_rn_gpui_loc}") + else() + message(WARNING "prebuilt: imported ${_rn_gpui_imp} has no IMPORTED_LOCATION; not installed") + endif() +endforeach() +if(_rn_gpui_imported_paths) + install(FILES ${_rn_gpui_imported_paths} DESTINATION lib) +endif() + +# --- headers -------------------------------------------------------------------- +# The archive ships no headers except OpenSSL's generated tree (Configure output, +# which consumers cannot produce without Perl). Every other header tree is named +# by a root variable RN_GPUI_PREBUILT_ROOT_ that the consumer sets to the +# source directory it already has (node_modules/react-native, the react-native-gpui +# package, its own cxxbridge and codegen output, an installed engine package, and +# the native-deps downloads). The targets file refers to those roots, so the +# config file checks they are set before anything is included. +install(DIRECTORY "${RN_GPUI_OPENSSL_INSTALL}/include/" + DESTINATION include/openssl-generated + FILES_MATCHING PATTERN "*.h") + +# Static include roots in search order. ReactCommonOverrides precedes ReactCommon +# so the override headers shadow the upstream ones, as the source build does. +set(_rn_gpui_include_roots + "\${RN_GPUI_PREBUILT_ROOT_react_native_gpui}" + "\${RN_GPUI_PREBUILT_ROOT_react_common_overrides}" + "\${RN_GPUI_PREBUILT_ROOT_react_common}" + "\${RN_GPUI_PREBUILT_ROOT_react_common}/jsi" + "\${RN_GPUI_PREBUILT_ROOT_react_common}/callinvoker" + "\${RN_GPUI_PREBUILT_ROOT_react_cxx_platform}" + "\${RN_GPUI_PREBUILT_ROOT_generated_rncore}" + "\${RN_GPUI_PREBUILT_ROOT_cxxbridge}" + "\${RN_GPUI_PREBUILT_ROOT_folly}" + "\${RN_GPUI_PREBUILT_ROOT_glog}" + "\${RN_GPUI_PREBUILT_ROOT_fmt}" + "\${RN_GPUI_PREBUILT_ROOT_double_conversion}" + "\${RN_GPUI_PREBUILT_ROOT_fast_float}" + "\${RN_GPUI_PREBUILT_ROOT_nlohmann_json}" + "\${RN_GPUI_PREBUILT_ROOT_boost}" + "\${RN_GPUI_PREBUILT_ROOT_curl}" + "\${RN_GPUI_PREBUILT_ROOT_openssl_generated}") + +# --- folly compile flags ---------------------------------------------------------- +# The source build has no folly-config.h. folly's flags (FOLLY_NO_CONFIG, +# FOLLY_HAVE_*, ...) are PUBLIC compile *options* on folly_runtime +# (-D= in folly/CMakeLists.txt), so they live in +# INTERFACE_COMPILE_OPTIONS. Split them: -D entries become compile definitions, +# everything else stays a compile option. Every exported target carries them, +# so consumers compile folly headers the same way the source build does. +set(_rn_gpui_folly_defs "") +set(_rn_gpui_folly_opts "") +if(TARGET folly_runtime) + get_target_property(_rn_gpui_folly_all folly_runtime INTERFACE_COMPILE_OPTIONS) + if(NOT _rn_gpui_folly_all) + set(_rn_gpui_folly_all "") + endif() + foreach(_rn_gpui_flag IN LISTS _rn_gpui_folly_all) + if(_rn_gpui_flag MATCHES "^-D(.+)$") + list(APPEND _rn_gpui_folly_defs "${CMAKE_MATCH_1}") + else() + list(APPEND _rn_gpui_folly_opts "${_rn_gpui_flag}") + endif() + endforeach() +endif() + +# --- include roots from the link closure ---------------------------------------- +# Source targets add their own include dirs (ReactCommon subdirectories such as +# react/renderer/graphics/platform/cxx and react/nativemodule/core). Map every dir +# that lies inside a source tree to "/", so +# the consumer resolves it against the tree it has. Longest source prefix wins. +# Pairs are "" "". +set(_rn_gpui_include_map + "${RN_GPUI_OVERRIDES_DIR}" "\${RN_GPUI_PREBUILT_ROOT_react_common_overrides}" + "${REACT_COMMON_DIR}" "\${RN_GPUI_PREBUILT_ROOT_react_common}" + "${REACT_CXX_PLATFORM_DIR}" "\${RN_GPUI_PREBUILT_ROOT_react_cxx_platform}" + "${CMAKE_CURRENT_SOURCE_DIR}" "\${RN_GPUI_PREBUILT_ROOT_react_native_gpui}" + "${RN_GPUI_CXXBRIDGE_INCLUDE_DIR}" "\${RN_GPUI_PREBUILT_ROOT_cxxbridge}" + "${RN_GPUI_GENERATED_RNCORE_DIR}/jni" "\${RN_GPUI_PREBUILT_ROOT_generated_rncore}" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly" "\${RN_GPUI_PREBUILT_ROOT_folly}" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/glog/exported" "\${RN_GPUI_PREBUILT_ROOT_glog}" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/fmt/include" "\${RN_GPUI_PREBUILT_ROOT_fmt}" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/double-conversion" "\${RN_GPUI_PREBUILT_ROOT_double_conversion}" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/fast_float/include" "\${RN_GPUI_PREBUILT_ROOT_fast_float}" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/nlohmann_json/include" "\${RN_GPUI_PREBUILT_ROOT_nlohmann_json}" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/boost/boost_1_83_0" "\${RN_GPUI_PREBUILT_ROOT_boost}" + "${RN_GPUI_CURL_SOURCE_DIR}/include" "\${RN_GPUI_PREBUILT_ROOT_curl}" + "${RN_GPUI_OPENSSL_INSTALL}/include" "\${RN_GPUI_PREBUILT_ROOT_openssl_generated}") + +# rn_gpui_prebuilt_include_ref( ): sets to the +# root-variable reference for (e.g. "${RN_GPUI_PREBUILT_ROOT_boost}/x"), +# or "" when the dir is not inside any mapped source tree. +function(rn_gpui_prebuilt_include_ref dir out_var) + # Source trees are often named with "..", e.g. REACT_COMMON_DIR is + # ${CMAKE_CURRENT_SOURCE_DIR}/../../..; targets report normalized paths. + # Normalize both sides so the prefix comparison can match. + get_filename_component(dir "${dir}" ABSOLUTE) + set(_best_len -1) + set(_best "") + list(LENGTH _rn_gpui_include_map _map_count) + math(EXPR _map_last "${_map_count} - 1") + foreach(_map_i RANGE 0 ${_map_last} 2) + list(GET _rn_gpui_include_map ${_map_i} _prefix) + if(NOT _prefix STREQUAL "") + get_filename_component(_prefix "${_prefix}" ABSOLUTE) + endif() + math(EXPR _installed_i "${_map_i} + 1") + list(GET _rn_gpui_include_map ${_installed_i} _installed) + string(LENGTH "${_prefix}" _plen) + # An unset source variable leaves an empty prefix; it must not match anything. + if(_plen EQUAL 0 OR NOT _plen GREATER _best_len) + continue() + endif() + string(LENGTH "${dir}" _dlen) + if(dir STREQUAL _prefix) + set(_rel "") + else() + if(NOT _dlen GREATER _plen) + continue() + endif() + string(SUBSTRING "${dir}" 0 ${_plen} _head) + string(SUBSTRING "${dir}" ${_plen} 1 _sep) + if(NOT _head STREQUAL _prefix OR NOT _sep STREQUAL "/") + continue() + endif() + math(EXPR _rest_at "${_plen} + 1") + string(SUBSTRING "${dir}" ${_rest_at} -1 _rel) + endif() + set(_best_len ${_plen}) + if(_rel STREQUAL "") + set(_best "${_installed}") + else() + set(_best "${_installed}/${_rel}") + endif() + endforeach() + set(${out_var} "${_best}" PARENT_SCOPE) +endfunction() + +# Walk the link closure and the shadow targets, collect every include dir they +# declare (BUILD_INTERFACE only; INSTALL_INTERFACE and unexpanded generator +# expressions are skipped), and map each one to its installed location. +set(_rn_gpui_mapped_includes "") +set(_rn_gpui_include_walk ${RN_GPUI_LINK_TARGETS} ${_rn_gpui_shadow}) +list(REMOVE_DUPLICATES _rn_gpui_include_walk) +foreach(_rn_gpui_walk_name IN LISTS _rn_gpui_include_walk) + if(NOT TARGET "${_rn_gpui_walk_name}") + continue() + endif() + foreach(_rn_gpui_prop IN ITEMS INTERFACE_INCLUDE_DIRECTORIES INTERFACE_SYSTEM_INCLUDE_DIRECTORIES) + get_target_property(_rn_gpui_dirs "${_rn_gpui_walk_name}" ${_rn_gpui_prop}) + if(NOT _rn_gpui_dirs) + continue() + endif() + foreach(_rn_gpui_dir IN LISTS _rn_gpui_dirs) + if(_rn_gpui_dir MATCHES "^\\$$") + set(_rn_gpui_dir "${CMAKE_MATCH_1}") + elseif(_rn_gpui_dir MATCHES "\\$<") + continue() + endif() + rn_gpui_prebuilt_include_ref("${_rn_gpui_dir}" _rn_gpui_inst) + if(_rn_gpui_inst) + list(APPEND _rn_gpui_mapped_includes "${_rn_gpui_inst}") + endif() + endforeach() + endforeach() +endforeach() +# Directory-level include_directories() calls (overrides, cpp/) reach targets +# only through the directory's INCLUDE_DIRECTORIES property, not their own +# INTERFACE properties. Map those too, so they stay on the consumer's path. +get_property(_rn_gpui_dir_incs DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" PROPERTY INCLUDE_DIRECTORIES) +foreach(_rn_gpui_dir IN LISTS _rn_gpui_dir_incs) + rn_gpui_prebuilt_include_ref("${_rn_gpui_dir}" _rn_gpui_inst) + if(_rn_gpui_inst) + list(APPEND _rn_gpui_mapped_includes "${_rn_gpui_inst}") + endif() +endforeach() +list(REMOVE_DUPLICATES _rn_gpui_mapped_includes) +foreach(_rn_gpui_inc IN LISTS _rn_gpui_mapped_includes) + list(APPEND _rn_gpui_include_roots "${_rn_gpui_inc}") +endforeach() +# Static roots first, so overrides (installed into include/react-common, which +# the mapped dirs also point at) keep their place ahead of upstream headers. +list(REMOVE_DUPLICATES _rn_gpui_include_roots) + +# --- targets file --------------------------------------------------------------- +# Generated with file(GENERATE) so $ resolves to the real +# archive name for this configuration. Paths use _rn_gpui_prebuilt_root, which +# the file computes from its own location, so the archive works unpacked anywhere. +set(_rn_gpui_targets_text +"# Generated by cpp/cmake/modules/PrebuiltInstall.cmake. Do not edit. +get_filename_component(_rn_gpui_prebuilt_root \"\${CMAKE_CURRENT_LIST_DIR}/..\" ABSOLUTE) +# OpenSSL's generated headers are the only headers in the archive. +set(RN_GPUI_PREBUILT_ROOT_openssl_generated \"\${_rn_gpui_prebuilt_root}/include/openssl-generated\") +set(_rn_gpui_prebuilt_include_roots \"${_rn_gpui_include_roots}\") +") + +# Every archive/dylib entry in the closure gets one IMPORTED target under the +# entry's own name (CURL::libcurl stays CURL::libcurl, even though its real +# target is libcurl_static), so INTERFACE_LINK_LIBRARIES below resolves in the +# consumer. Built entries point at $ for this configuration; +# imported entries use the file name configure resolved. +foreach(_rn_gpui_entry IN LISTS _rn_gpui_order) + if(_rn_gpui_entry STREQUAL "rn_gpui_core") + continue() + endif() + get_target_property(_rn_gpui_aliased "${_rn_gpui_entry}" ALIASED_TARGET) + if(_rn_gpui_aliased) + set(_rn_gpui_real "${_rn_gpui_aliased}") + else() + set(_rn_gpui_real "${_rn_gpui_entry}") + endif() + get_target_property(_rn_gpui_imported_flag "${_rn_gpui_real}" IMPORTED) + get_target_property(_rn_gpui_real_type "${_rn_gpui_real}" TYPE) + if(_rn_gpui_real_type STREQUAL "SHARED_LIBRARY") + set(_rn_gpui_kind "SHARED") + elseif(_rn_gpui_real_type STREQUAL "UNKNOWN_LIBRARY") + set(_rn_gpui_kind "UNKNOWN") + else() + set(_rn_gpui_kind "STATIC") + endif() + if(_rn_gpui_imported_flag) + get_target_property(_rn_gpui_loc "${_rn_gpui_real}" IMPORTED_LOCATION) + if(NOT _rn_gpui_loc OR NOT EXISTS "${_rn_gpui_loc}") + message(WARNING "prebuilt: imported ${_rn_gpui_real} has no IMPORTED_LOCATION; skipped") + continue() + endif() + get_filename_component(_rn_gpui_base "${_rn_gpui_loc}" NAME) + set(_rn_gpui_file "${_rn_gpui_base}") + else() + set(_rn_gpui_file "$") + endif() + # A built shared library links through its import library, which the install + # rules above put in `lib/` while the DLL itself goes to `bin/`. Emitting only + # IMPORTED_LOCATION left the consumer with nothing to link against: CMake + # resolved the target to `jsi-NOTFOUND` and the link emitted an empty `-L`, + # failing with "empty search path given via `-L`". Name the DLL in `bin/` and + # the import library in `lib/`. + # + # Only built targets: an imported shared library is installed with + # `install(FILES ... DESTINATION lib)` and has no import library in the + # archive, so its single file keeps naming `lib/`. + if(_rn_gpui_kind STREQUAL "SHARED" AND NOT _rn_gpui_imported_flag) + string(APPEND _rn_gpui_targets_text +"add_library(${_rn_gpui_entry} ${_rn_gpui_kind} IMPORTED) +set_target_properties(${_rn_gpui_entry} PROPERTIES + IMPORTED_LOCATION \"\${_rn_gpui_prebuilt_root}/bin/${_rn_gpui_file}\" + IMPORTED_IMPLIB \"\${_rn_gpui_prebuilt_root}/lib/$\") +") + continue() + endif() + string(APPEND _rn_gpui_targets_text +"add_library(${_rn_gpui_entry} ${_rn_gpui_kind} IMPORTED) +set_target_properties(${_rn_gpui_entry} PROPERTIES + IMPORTED_LOCATION \"\${_rn_gpui_prebuilt_root}/lib/${_rn_gpui_file}\") +") +endforeach() + +# Shadow targets: INTERFACE, include roots, folly defs, and their own defs. +# They link rn_gpui_core, which already carries every archive. +foreach(_rn_gpui_shadow_name IN LISTS _rn_gpui_shadow) + get_target_property(_rn_gpui_own_defs "${_rn_gpui_shadow_name}" INTERFACE_COMPILE_DEFINITIONS) + if(NOT _rn_gpui_own_defs) + set(_rn_gpui_own_defs "") + endif() + set(_rn_gpui_defs ${_rn_gpui_folly_defs} ${_rn_gpui_own_defs}) + list(REMOVE_DUPLICATES _rn_gpui_defs) + string(APPEND _rn_gpui_targets_text +"add_library(${_rn_gpui_shadow_name} INTERFACE IMPORTED) +set_target_properties(${_rn_gpui_shadow_name} PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES \"\${_rn_gpui_prebuilt_include_roots}\" + INTERFACE_COMPILE_DEFINITIONS \"${_rn_gpui_defs}\" + INTERFACE_COMPILE_OPTIONS \"${_rn_gpui_folly_opts}\" + INTERFACE_LINK_LIBRARIES rn_gpui_core) +") +endforeach() + +# rn_gpui_core links the real archives in the closure's order, which is the +# order static archives must be resolved in. +set(_rn_gpui_core_links "") +foreach(_rn_gpui_entry IN LISTS _rn_gpui_order) + if(NOT _rn_gpui_entry STREQUAL "rn_gpui_core") + list(APPEND _rn_gpui_core_links "${_rn_gpui_entry}") + endif() +endforeach() +list(REMOVE_DUPLICATES _rn_gpui_core_links) +get_target_property(_rn_gpui_core_defs rn_gpui_core INTERFACE_COMPILE_DEFINITIONS) +if(NOT _rn_gpui_core_defs) + set(_rn_gpui_core_defs "") +endif() +set(_rn_gpui_core_all_defs ${_rn_gpui_folly_defs} ${_rn_gpui_core_defs}) +list(REMOVE_DUPLICATES _rn_gpui_core_all_defs) +string(APPEND _rn_gpui_targets_text +"add_library(rn_gpui_core STATIC IMPORTED) +set_target_properties(rn_gpui_core PROPERTIES + IMPORTED_LOCATION \"\${_rn_gpui_prebuilt_root}/lib/$\" + INTERFACE_INCLUDE_DIRECTORIES \"\${_rn_gpui_prebuilt_include_roots}\" + INTERFACE_COMPILE_DEFINITIONS \"${_rn_gpui_core_all_defs}\" + INTERFACE_COMPILE_OPTIONS \"${_rn_gpui_folly_opts}\" + INTERFACE_LINK_LIBRARIES \"${_rn_gpui_core_links}\") +") + +# The targets file is evaluated per configuration: `$` +# differs between Debug and Release on MSVC (`libcurl-d.lib` vs `libcurl.lib`). +# A multi-config generator (Visual Studio) therefore rejects a fixed output name +# ("Evaluation file to be written multiple times with different content"), so the +# configuration goes into the generated name there. The install step below picks +# the one that belongs to the configuration being installed and renames it back, +# so the installed layout is identical on every platform. +if(CMAKE_CONFIGURATION_TYPES) + set(_rn_gpui_targets_suffix "-$") +else() + set(_rn_gpui_targets_suffix "") +endif() +set(_rn_gpui_targets_file + "${_rn_gpui_prebuilt_gen}/${RN_GPUI_PREBUILT_CONFIG_NAME}-targets${_rn_gpui_targets_suffix}.cmake") +file(GENERATE OUTPUT "${_rn_gpui_targets_file}" + CONTENT "${_rn_gpui_targets_text}") + +# --- config file -------------------------------------------------------------------- +# Every root the targets file can reference. The consumer must set each one to +# an existing directory before find_package; openssl_generated is set by the +# targets file itself. +set(_rn_gpui_required_roots + react_native_gpui react_common_overrides react_common react_cxx_platform + generated_rncore cxxbridge folly glog fmt double_conversion fast_float + nlohmann_json boost curl) +set(_rn_gpui_root_checks "") +foreach(_rn_gpui_root IN LISTS _rn_gpui_required_roots) + string(APPEND _rn_gpui_root_checks +"if(NOT DEFINED RN_GPUI_PREBUILT_ROOT_${_rn_gpui_root} OR NOT EXISTS \"\${RN_GPUI_PREBUILT_ROOT_${_rn_gpui_root}}\") + message(FATAL_ERROR \"${RN_GPUI_PREBUILT_CONFIG_NAME}: RN_GPUI_PREBUILT_ROOT_${_rn_gpui_root} must be set to an existing directory\") +endif() +") +endforeach() +file(WRITE "${_rn_gpui_prebuilt_gen}/${RN_GPUI_PREBUILT_CONFIG_NAME}-config.cmake" +"# Consumer entry point: find_package(${RN_GPUI_PREBUILT_CONFIG_NAME} CONFIG REQUIRED) +# Set RN_GPUI_PREBUILT_ROOT_ for each name below before calling it. +${_rn_gpui_root_checks}include(\"\${CMAKE_CURRENT_LIST_DIR}/${RN_GPUI_PREBUILT_CONFIG_NAME}-targets.cmake\") +set(${RN_GPUI_PREBUILT_CONFIG_NAME}_FOUND TRUE) +") + +install(FILES + "${_rn_gpui_prebuilt_gen}/${RN_GPUI_PREBUILT_CONFIG_NAME}-config.cmake" + DESTINATION cmake) +# `$` in the source name resolves to the configuration being installed, +# and RENAME gives every platform the same installed file name. +install(FILES "${_rn_gpui_targets_file}" + DESTINATION cmake + RENAME "${RN_GPUI_PREBUILT_CONFIG_NAME}-targets.cmake") diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/modules/ThirdParty.cmake b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/modules/ThirdParty.cmake new file mode 100644 index 0000000000..baa3db4bda --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/modules/ThirdParty.cmake @@ -0,0 +1,84 @@ +add_subdirectory("${RN_GPUI_NATIVE_DEPS_PREPARED}/glog" "${RN_GPUI_NATIVE_DEPS_BUILD}/glog") +if(WIN32) + # glog's utilities.h includes "port.h" when OS_WINDOWS is defined, and that + # header lives in glog-/src/windows. RN's Android-first CMakeLists + # only exposes the project root and exported/ to the compiler. + file(GLOB _rn_gpui_glog_windows_include + "${RN_GPUI_NATIVE_DEPS_PREPARED}/glog/glog-*/src/windows") + if(_rn_gpui_glog_windows_include) + target_include_directories(glog PRIVATE ${_rn_gpui_glog_windows_include}) + endif() + # glog 0.3.5 predates VS2015's C99 snprintf and would declare its own + # (C2375: redefinition; different linkage); HAVE_SNPRINTF skips that + # declaration. Its symbolize.cc uses the DbgHelp API. + target_compile_definitions(glog PRIVATE HAVE_SNPRINTF=1) + target_link_libraries(glog PUBLIC dbghelp) + # ReactAndroid's glog source list omits src/windows/port.cc, which provides + # safe_vsnprintf (used by raw_logging) and the other Windows shims. + file(GLOB _rn_gpui_glog_windows_sources + "${RN_GPUI_NATIVE_DEPS_PREPARED}/glog/glog-*/src/windows/*.cc") + if(_rn_gpui_glog_windows_sources) + target_sources(glog PRIVATE ${_rn_gpui_glog_windows_sources}) + endif() +endif() +add_subdirectory("${RN_GPUI_NATIVE_DEPS_PREPARED}/double-conversion" "${RN_GPUI_NATIVE_DEPS_BUILD}/double-conversion") +add_subdirectory("${RN_GPUI_NATIVE_DEPS_PREPARED}/fast_float" "${RN_GPUI_NATIVE_DEPS_BUILD}/fast_float") +add_subdirectory("${RN_GPUI_NATIVE_DEPS_PREPARED}/fmt" "${RN_GPUI_NATIVE_DEPS_BUILD}/fmt") +add_subdirectory("${RN_GPUI_NATIVE_DEPS_PREPARED}/folly" "${RN_GPUI_NATIVE_DEPS_BUILD}/folly") +# RN's reduced Folly target omits platform-neutral sources used by +# ReactCxxPlatform's HTTP and threading implementations on desktop targets. +target_sources(folly_runtime PRIVATE + "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly/folly/system/ThreadName.cpp" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly/folly/io/IOBuf.cpp" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly/folly/Uri.cpp" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly/folly/memory/SanitizeAddress.cpp") +if(WIN32) + # On Windows folly routes POSIX calls (close, dup, sysconf, open, mmap, + # getrusage) through folly/portability/*.cpp, which Android never needs. + # Unistd.cpp closes sockets through SocketFileDescriptorMap, and MSVC + # references the leak annotations that clang folds away. + file(GLOB _rn_gpui_folly_portability + "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly/folly/portability/*.cpp") + list(FILTER _rn_gpui_folly_portability EXCLUDE REGEX "/SysUio\\.cpp$") + target_sources(folly_runtime PRIVATE + ${_rn_gpui_folly_portability} + "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly/folly/net/detail/SocketFileDescriptorMap.cpp" + "${RN_GPUI_NATIVE_DEPS_PREPARED}/folly/folly/memory/SanitizeLeak.cpp") + target_link_libraries(folly_runtime ws2_32) + # portability/PThread.cpp implements pthread keys with + # boost::thread_specific_ptr, which lives in compiled Boost.Thread. Only + # its Win32 TSS sources are needed; tss_pe.cpp runs the per-thread cleanup. + set(_rn_gpui_boost_thread + "${RN_GPUI_NATIVE_DEPS_SOURCES}/boost/boost_1_83_0/libs/thread/src/win32") + target_sources(folly_runtime PRIVATE + "${_rn_gpui_boost_thread}/thread.cpp" + "${_rn_gpui_boost_thread}/thread_primitives.cpp" + "${_rn_gpui_boost_thread}/tss_pe.cpp") + set_source_files_properties( + "${_rn_gpui_boost_thread}/thread.cpp" + "${_rn_gpui_boost_thread}/thread_primitives.cpp" + "${_rn_gpui_boost_thread}/tss_pe.cpp" + TARGET_DIRECTORY folly_runtime + PROPERTIES COMPILE_DEFINITIONS "BOOST_THREAD_BUILD_LIB;BOOST_CHRONO_HEADER_ONLY") +endif() + +add_library(boost INTERFACE) +target_include_directories(boost INTERFACE + "${RN_GPUI_NATIVE_DEPS_PREPARED}/boost/boost_1_83_0" + "${RN_GPUI_OPENSSL_INSTALL}/include") + +add_library(nlohmann_json INTERFACE) +target_include_directories(nlohmann_json INTERFACE + "${RN_GPUI_NATIVE_DEPS_PREPARED}/nlohmann_json/include") +add_library(double_conversion ALIAS double-conversion) + +# folly -> folly_runtime (RN's name for this dependency). +# fbgloginit and fbjni are Android/JNI-only. +add_library(glog_init INTERFACE) +add_library(fbjni INTERFACE) + +find_package(OpenSSL REQUIRED) + +# The Hermes VM and the React Native Hermes glue live in the engine package +# @react-native-gpui/hermes/desktop/CMakeLists.txt, which autolink adds when the +# package is installed. Core has no engine of its own. diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/msvc-preinclude.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/msvc-preinclude.h new file mode 100644 index 0000000000..556dfaefbd --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/cmake/msvc-preinclude.h @@ -0,0 +1,59 @@ +// Standard headers that MSVC's standard library does not pull in +// transitively the way libc++ does. +// +// ReactCommon (and folly) rely on the libc++ behaviour in places, for example +// jsinspector-modern/HostTarget.h uses std::mutex without including . +// This header is force-included into every C++ translation unit on MSVC +// (`/FI`), mirroring the `-include cstdint` the Linux build already needs. +#pragma once + +/* M_PI and friends are only exposed when this is defined before . */ +#ifndef _USE_MATH_DEFINES +#define _USE_MATH_DEFINES +#endif +#include + +/* ReactCommon calls __builtin_trap() in a few places. */ +#if !defined(__builtin_trap) +#define __builtin_trap() __debugbreak() +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Additional headers MSVC does not reach transitively from ReactCommon's own +// includes (react-native-windows force-includes the same set through its PCH). +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/jni/CMakeLists.txt b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/jni/CMakeLists.txt new file mode 100644 index 0000000000..c19b292769 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/jni/CMakeLists.txt @@ -0,0 +1,32 @@ +# The build script runs the pinned React Native @react-native/codegen into +# Cargo's OUT_DIR. Consume its Android component/C++ TurboModule output here; +# Android Java and JNI implementation sources are intentionally not built. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +set(RN_GPUI_CODEGEN_JNI_DIR "${RN_GPUI_GENERATED_RNCORE_DIR}/jni") +file(GLOB react_codegen_SRCS CONFIGURE_DEPENDS + "${RN_GPUI_CODEGEN_JNI_DIR}/react/renderer/components/FBReactNativeSpec/*.cpp") + +add_library( + react_codegen_rncore + OBJECT + ${react_codegen_SRCS} +) + +target_include_directories(react_codegen_rncore PUBLIC + "${RN_GPUI_CODEGEN_JNI_DIR}" + "${RN_GPUI_CODEGEN_JNI_DIR}/react/renderer/components/FBReactNativeSpec" + "${RN_GPUI_CODEGEN_JNI_DIR}/react/renderer/components" +) + +target_link_libraries( + react_codegen_rncore + jsi + # We need to link different libraries based on whether we are building rncore or not, that's necessary + # because we want to break a circular dependency between react_codegen_rncore and reactnative + folly_runtime glog react_debug react_nativemodule_core react_renderer_componentregistry react_renderer_core react_renderer_debug react_renderer_graphics react_renderer_imagemanager react_renderer_mapbuffer react_utils rrc_image rrc_view yoga +) + +target_compile_reactnative_options(react_codegen_rncore PRIVATE) diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/rn_gpui_linux_prefix.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/rn_gpui_linux_prefix.h new file mode 100644 index 0000000000..757685ece1 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/rn_gpui_linux_prefix.h @@ -0,0 +1,17 @@ +#pragma once + +// Force-included on Linux (see CMakeLists.txt), before every C++ translation +// unit in this tree. React Native's headers and the packages it bundles rely on +// the libc++ headers shipping transitively; GCC/libstdc++ does not. +// +// - RN 0.87.1 uses `uint*_t` without including in several headers. +// `CSS/common/transforms/vectors.h` uses bare `size_t` and only gets it +// from , which it includes *only when NDEBUG is undefined*. A +// Release build defines NDEBUG, so that header lost `size_t` and the build +// failed with "'size_t' has not been declared" in a third-party file. +// +// Both are declared here instead of patching the packages. It is one header so +// the compiler is given a single `-include` argument: CMake de-duplicates +// repeated options, and a second `-include
` pair collapses. +#include +#include diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/test/gpui_js_engine_test.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/test/gpui_js_engine_test.cpp new file mode 100644 index 0000000000..ecf9e76269 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/test/gpui_js_engine_test.cpp @@ -0,0 +1,143 @@ +// Unit test for the JS engine registry and its selection rules +//. +// +// The registry is process-global and registration is startup-only, so the cases +// run in the order the rules apply: no engine, one engine, several engines with +// and without an explicit selection, and an unknown name. +#include "GpuiJsEngine.h" + +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool condition, const std::string& what) { + if (!condition) { + ++failures; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + } +} + +// Runs `body` and reports whether it threw a std::exception whose message +// contains `expected`. +template +void checkThrows( + Body body, + const std::string& expected, + const std::string& what) { + try { + body(); + } catch (const std::exception& error) { + if (std::string(error.what()).find(expected) == std::string::npos) { + ++failures; + std::fprintf( + stderr, + "FAIL: %s (message %s does not contain %s)\n", + what.c_str(), + error.what(), + expected.c_str()); + } + return; + } catch (...) { + ++failures; + std::fprintf(stderr, "FAIL: %s (threw the wrong type)\n", what.c_str()); + return; + } + ++failures; + std::fprintf(stderr, "FAIL: %s (did not throw)\n", what.c_str()); +} + +rngpui::GpuiJsEngine makeEngine(const std::string& name) { + return rngpui::GpuiJsEngine{ + .info = {.name = name, .version = "1.2.3"}, + .makeRuntimeFactory = [] { return nullptr; }, + }; +} + +} // namespace + +int main() { + using rngpui::activeJsEngine; + using rngpui::registeredJsEngines; + using rngpui::registerJsEngine; + using rngpui::selectJsEngine; + + // No engine installed. + check(registeredJsEngines().empty(), "no engines are registered yet"); + checkThrows( + [] { activeJsEngine(); }, + "no JS engine installed", + "activeJsEngine with no engine"); + + // One engine: it is the default. + registerJsEngine(makeEngine("alpha")); + check(registeredJsEngines().size() == 1, "one engine registered"); + check( + activeJsEngine().info.name == "alpha", "the only engine is the default"); + check(activeJsEngine().info.version == "1.2.3", "the version is reported"); + + // Several engines: ambiguous until one is selected. + registerJsEngine(makeEngine("beta")); + check(registeredJsEngines().size() == 2, "two engines registered"); + checkThrows( + [] { activeJsEngine(); }, + "several JS engines are installed", + "activeJsEngine with two engines and no selection"); + checkThrows( + [] { activeJsEngine(); }, + "alpha, beta", + "the ambiguity message lists the registered engines"); + + // Several engines with a selection. + selectJsEngine("beta"); + check(activeJsEngine().info.name == "beta", "the selection wins"); + selectJsEngine("alpha"); + check(activeJsEngine().info.name == "alpha", "the selection can change"); + selectJsEngine(""); + checkThrows( + [] { activeJsEngine(); }, + "several JS engines", + "an empty selection returns to the ambiguous default"); + + // Unknown name. + checkThrows( + [] { selectJsEngine("gamma"); }, + "unknown JS engine 'gamma'", + "selectJsEngine with an unknown name"); + checkThrows( + [] { selectJsEngine("gamma"); }, + "alpha, beta", + "the unknown-name message lists the registered engines"); + + // Registration is validated. + checkThrows( + [] { registerJsEngine(makeEngine("")); }, + "must report a name", + "registerJsEngine with an empty name"); + checkThrows( + [] { registerJsEngine(makeEngine("alpha")); }, + "duplicate JS engine 'alpha'", + "registerJsEngine with a duplicate name"); + checkThrows( + [] { + registerJsEngine(rngpui::GpuiJsEngine{ + .info = {.name = "gamma", .version = "1"}, + }); + }, + "no runtime factory", + "registerJsEngine without a runtime factory"); + + // A failed registration must not have been recorded. + selectJsEngine("alpha"); + check(activeJsEngine().info.name == "alpha", "the registry is unchanged"); + + if (failures == 0) { + std::printf("gpui_js_engine_test: all checks passed\n"); + return 0; + } + std::fprintf(stderr, "gpui_js_engine_test: %d check(s) failed\n", failures); + return 1; +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/test/gpui_turbo_module_test.cpp b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/test/gpui_turbo_module_test.cpp new file mode 100644 index 0000000000..c542a8507c --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/test/gpui_turbo_module_test.cpp @@ -0,0 +1,117 @@ +// Unit test for the module task queue: ordering, that work leaves the caller's +// thread, and that destruction never hangs. +#include "GpuiTurboModule.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/// Stands in for the Rust side of `rngpui::dispatchMain`, which this test does +/// not link. +namespace rngpui { +void rn_gpui_run_on_main(uint64_t) noexcept {} +} // namespace rngpui + +namespace { + +int failures = 0; + +void check(bool condition, const std::string& what) { + if (!condition) { + ++failures; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + } +} + +void checkRunsInOrderOnOneThread() { + rngpui::ModuleQueue queue("rn.module.Test"); + const auto caller = std::this_thread::get_id(); + std::mutex mutex; + std::condition_variable done; + std::vector order; + std::vector threads; + for (int index = 0; index < 8; ++index) { + queue.post([&, index] { + std::lock_guard lock(mutex); + order.push_back(index); + threads.push_back(std::this_thread::get_id()); + if (order.size() == 8) { + done.notify_one(); + } + }); + } + { + std::unique_lock lock(mutex); + done.wait_for( + lock, std::chrono::seconds(10), [&] { return order.size() == 8; }); + } + check(order.size() == 8, "every posted task ran"); + for (size_t index = 0; index < order.size(); ++index) { + check( + order[index] == static_cast(index), + "tasks run in the order they were posted"); + } + check( + !threads.empty() && threads.front() != caller, + "the worker is not the posting thread"); + for (const auto thread : threads) { + check(thread == threads.front(), "all tasks run on one worker thread"); + } +} + +void checkDestructionDiscardsPendingTasks() { + std::atomic finished{0}; + { + rngpui::ModuleQueue queue("rn.module.Test"); + queue.post([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + finished.fetch_add(1); + }); + for (int index = 0; index < 32; ++index) { + queue.post([&] { finished.fetch_add(1); }); + } + } + check( + finished.load() <= 1, + "tasks still pending when the queue is destroyed are discarded"); +} + +void checkDestructionFromWorkerDoesNotDeadlock() { + // The task deletes the queue, so the destructor runs on the worker thread. + auto* queue = new rngpui::ModuleQueue("rn.module.Test"); + std::mutex mutex; + std::condition_variable destroyed; + bool destroyedFlag = false; + queue->post([queue, &mutex, &destroyed, &destroyedFlag] { + delete queue; + { + std::lock_guard lock(mutex); + destroyedFlag = true; + } + destroyed.notify_one(); + }); + std::unique_lock lock(mutex); + const auto destroyedInTime = destroyed.wait_for( + lock, std::chrono::seconds(10), [&] { return destroyedFlag; }); + check(destroyedInTime, "destroying the queue from its worker does not hang"); +} + +} // namespace + +int main() { + checkRunsInOrderOnOneThread(); + checkDestructionDiscardsPendingTasks(); + checkDestructionFromWorkerDoesNotDeadlock(); + if (failures != 0) { + std::fprintf(stderr, "%d check(s) failed\n", failures); + return 1; + } + std::printf("gpui_turbo_module_test: all checks passed\n"); + return 0; +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/ascii_number.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/ascii_number.h new file mode 100644 index 0000000000..e814f12f13 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/ascii_number.h @@ -0,0 +1,903 @@ +#ifndef FASTFLOAT_ASCII_NUMBER_H +#define FASTFLOAT_ASCII_NUMBER_H + +#include +#include +#include +#include +#include +#include + +#include "float_common.h" + +#ifdef FASTFLOAT_SSE2 +#include +#endif + +#ifdef FASTFLOAT_NEON +#include +#endif + +namespace fast_float { + +template fastfloat_really_inline constexpr bool has_simd_opt() { +#ifdef FASTFLOAT_HAS_SIMD + return std::is_same::value; +#else + return false; +#endif +} + +// Next function can be micro-optimized, but compilers are entirely +// able to optimize it well. +template +fastfloat_really_inline constexpr bool is_integer(UC c) noexcept { + return static_cast(c - UC('0')) <= 9u; +} + +fastfloat_really_inline constexpr uint64_t byteswap(uint64_t val) { + return (val & 0xFF00000000000000) >> 56 | (val & 0x00FF000000000000) >> 40 | + (val & 0x0000FF0000000000) >> 24 | (val & 0x000000FF00000000) >> 8 | + (val & 0x00000000FF000000) << 8 | (val & 0x0000000000FF0000) << 24 | + (val & 0x000000000000FF00) << 40 | (val & 0x00000000000000FF) << 56; +} + +fastfloat_really_inline constexpr uint32_t byteswap_32(uint32_t val) { + return (val >> 24) | ((val >> 8) & 0x0000FF00u) | ((val << 8) & 0x00FF0000u) | + (val << 24); +} + +// Read 8 UC into a u64. Truncates UC if not char. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 uint64_t +read8_to_u64(UC const *chars) { + if (cpp20_and_in_constexpr() || !std::is_same::value) { + uint64_t val = 0; + for (int i = 0; i < 8; ++i) { + val |= uint64_t(uint8_t(*chars)) << (i * 8); + ++chars; + } + return val; + } + uint64_t val; + ::memcpy(&val, chars, sizeof(uint64_t)); +#if FASTFLOAT_IS_BIG_ENDIAN == 1 + // Need to read as-if the number was in little-endian order. + val = byteswap(val); +#endif + return val; +} + +// Read 4 UC into a u32. Truncates UC if not char. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 uint32_t +read4_to_u32(UC const *chars) { + if (cpp20_and_in_constexpr() || !std::is_same::value) { + uint32_t val = 0; + for (int i = 0; i < 4; ++i) { + val |= uint32_t(uint8_t(*chars)) << (i * 8); + ++chars; + } + return val; + } + uint32_t val; + ::memcpy(&val, chars, sizeof(uint32_t)); +#if FASTFLOAT_IS_BIG_ENDIAN == 1 + val = byteswap_32(val); +#endif + return val; +} +#ifdef FASTFLOAT_SSE2 + +fastfloat_really_inline uint64_t simd_read8_to_u64(__m128i const data) { + FASTFLOAT_SIMD_DISABLE_WARNINGS + __m128i const packed = _mm_packus_epi16(data, data); +#ifdef FASTFLOAT_64BIT + return uint64_t(_mm_cvtsi128_si64(packed)); +#else + uint64_t value; + // Visual Studio + older versions of GCC don't support _mm_storeu_si64 + _mm_storel_epi64(reinterpret_cast<__m128i *>(&value), packed); + return value; +#endif + FASTFLOAT_SIMD_RESTORE_WARNINGS +} + +fastfloat_really_inline uint64_t simd_read8_to_u64(char16_t const *chars) { + FASTFLOAT_SIMD_DISABLE_WARNINGS + return simd_read8_to_u64( + _mm_loadu_si128(reinterpret_cast<__m128i const *>(chars))); + FASTFLOAT_SIMD_RESTORE_WARNINGS +} + +#elif defined(FASTFLOAT_NEON) + +fastfloat_really_inline uint64_t simd_read8_to_u64(uint16x8_t const data) { + FASTFLOAT_SIMD_DISABLE_WARNINGS + uint8x8_t utf8_packed = vmovn_u16(data); + return vget_lane_u64(vreinterpret_u64_u8(utf8_packed), 0); + FASTFLOAT_SIMD_RESTORE_WARNINGS +} + +fastfloat_really_inline uint64_t simd_read8_to_u64(char16_t const *chars) { + FASTFLOAT_SIMD_DISABLE_WARNINGS + return simd_read8_to_u64( + vld1q_u16(reinterpret_cast(chars))); + FASTFLOAT_SIMD_RESTORE_WARNINGS +} + +#endif // FASTFLOAT_SSE2 + +// MSVC SFINAE is broken pre-VS2017 +#if defined(_MSC_VER) && _MSC_VER <= 1900 +template +#else +template ()) = 0> +#endif +// dummy for compile +uint64_t simd_read8_to_u64(UC const *) { + return 0; +} + +// credit @aqrit +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 uint32_t +parse_eight_digits_unrolled(uint64_t val) { + uint64_t const mask = 0x000000FF000000FF; + uint64_t const mul1 = 0x000F424000000064; // 100 + (1000000ULL << 32) + uint64_t const mul2 = 0x0000271000000001; // 1 + (10000ULL << 32) + val -= 0x3030303030303030; + val = (val * 10) + (val >> 8); // val = (val * 2561) >> 8; + val = (((val & mask) * mul1) + (((val >> 16) & mask) * mul2)) >> 32; + return uint32_t(val); +} + +// Call this if chars are definitely 8 digits. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 uint32_t +parse_eight_digits_unrolled(UC const *chars) noexcept { + if (cpp20_and_in_constexpr() || !has_simd_opt()) { + return parse_eight_digits_unrolled(read8_to_u64(chars)); // truncation okay + } + return parse_eight_digits_unrolled(simd_read8_to_u64(chars)); +} + +// credit @aqrit +fastfloat_really_inline constexpr bool +is_made_of_eight_digits_fast(uint64_t val) noexcept { + return !((((val + 0x4646464646464646) | (val - 0x3030303030303030)) & + 0x8080808080808080)); +} + +fastfloat_really_inline constexpr bool +is_made_of_four_digits_fast(uint32_t val) noexcept { + return !((((val + 0x46464646) | (val - 0x30303030)) & 0x80808080)); +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 uint32_t +parse_four_digits_unrolled(uint32_t val) noexcept { + val -= 0x30303030; + val = (val * 10) + (val >> 8); + return (((val & 0x00FF00FF) * 0x00640001) >> 16) & 0xFFFF; +} + +#ifdef FASTFLOAT_HAS_SIMD + +// Call this if chars might not be 8 digits. +// Using this style (instead of is_made_of_eight_digits_fast() then +// parse_eight_digits_unrolled()) ensures we don't load SIMD registers twice. +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool +simd_parse_if_eight_digits_unrolled(char16_t const *chars, + uint64_t &i) noexcept { + if (cpp20_and_in_constexpr()) { + return false; + } +#ifdef FASTFLOAT_SSE2 + FASTFLOAT_SIMD_DISABLE_WARNINGS + __m128i const data = + _mm_loadu_si128(reinterpret_cast<__m128i const *>(chars)); + + // (x - '0') <= 9 + // http://0x80.pl/articles/simd-parsing-int-sequences.html + __m128i const t0 = _mm_add_epi16(data, _mm_set1_epi16(32720)); + __m128i const t1 = _mm_cmpgt_epi16(t0, _mm_set1_epi16(-32759)); + + if (_mm_movemask_epi8(t1) == 0) { + i = i * 100000000 + parse_eight_digits_unrolled(simd_read8_to_u64(data)); + return true; + } else + return false; + FASTFLOAT_SIMD_RESTORE_WARNINGS +#elif defined(FASTFLOAT_NEON) + FASTFLOAT_SIMD_DISABLE_WARNINGS + uint16x8_t const data = vld1q_u16(reinterpret_cast(chars)); + + // (x - '0') <= 9 + // http://0x80.pl/articles/simd-parsing-int-sequences.html + uint16x8_t const t0 = vsubq_u16(data, vmovq_n_u16('0')); + uint16x8_t const mask = vcltq_u16(t0, vmovq_n_u16('9' - '0' + 1)); + + if (vminvq_u16(mask) == 0xFFFF) { + i = i * 100000000 + parse_eight_digits_unrolled(simd_read8_to_u64(data)); + return true; + } else + return false; + FASTFLOAT_SIMD_RESTORE_WARNINGS +#else + static_cast(chars); + static_cast(i); + return false; +#endif // FASTFLOAT_SSE2 +} + +#endif // FASTFLOAT_HAS_SIMD + +// MSVC SFINAE is broken pre-VS2017 +#if defined(_MSC_VER) && _MSC_VER <= 1900 +template +#else +template ()) = 0> +#endif +// dummy for compile +bool simd_parse_if_eight_digits_unrolled(UC const *, uint64_t &) { + return 0; +} + +template ::value) = 0> +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 void +loop_parse_if_eight_digits(UC const *&p, UC const *const pend, uint64_t &i) { + if (!has_simd_opt()) { + return; + } + while ((std::distance(p, pend) >= 8) && + simd_parse_if_eight_digits_unrolled( + p, i)) { // in rare cases, this will overflow, but that's ok + p += 8; + } +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 void +loop_parse_if_eight_digits(char const *&p, char const *const pend, + uint64_t &i) { + // optimizes better than parse_if_eight_digits_unrolled() for UC = char. + while ((std::distance(p, pend) >= 8) && + is_made_of_eight_digits_fast(read8_to_u64(p))) { + i = i * 100000000 + + parse_eight_digits_unrolled(read8_to_u64( + p)); // in rare cases, this will overflow, but that's ok + p += 8; + } + // Consume a remaining 4-7 digit run in a single SWAR step instead of + // byte-by-byte (reuses the existing 4-digit helpers). The parsed result is + // identical either way. Historically gated to clang because gcc regressed on + // short remainders, but that verdict predates the span-elision restructure; + // with the leaner hot path the 4-digit step now wins on gcc as well. + if ((pend - p) >= 4) { + uint32_t const val4 = read4_to_u32(p); + if (is_made_of_four_digits_fast(val4)) { + i = i * 10000 + + parse_four_digits_unrolled(val4); // may overflow, that's ok + p += 4; + } + } +} + +enum class parse_error { + no_error, + // [JSON-only] The minus sign must be followed by an integer. + missing_integer_after_sign, + // A sign must be followed by an integer or dot. + missing_integer_or_dot_after_sign, + // [JSON/JavaScript-only] The integer part must not have leading zeros. + leading_zeros_in_integer_part, + // [JSON-only] The integer part must have at least one digit. + no_digits_in_integer_part, + // [JSON-only] If there is a decimal point, there must be digits in the + // fractional part. + no_digits_in_fractional_part, + // The mantissa must have at least one digit. + no_digits_in_mantissa, + // Scientific notation requires an exponential part. + missing_exponential_part, + // [JavaScript-only, sloppy mode] The integer part is a legacy octal literal + // (a leading zero followed by octal digits only), which is not a decimal + // number. Parse it in base 8 instead. + legacy_octal_integer_part, +}; + +template struct parsed_number_string_t { + int64_t exponent{0}; + uint64_t mantissa{0}; + UC const *lastmatch{nullptr}; + bool negative{false}; + bool valid{false}; + bool too_many_digits{false}; + // contains the range of the significant digits + span integer{}; // non-nullable + span fraction{}; // nullable + parse_error error{parse_error::no_error}; +}; + +using byte_span = span; +using parsed_number_string = parsed_number_string_t; + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t +report_parse_error(UC const *p, parse_error error) { + parsed_number_string_t answer; + answer.valid = false; + answer.lastmatch = p; + answer.error = error; + return answer; +} + +// Assuming that you use no more than 19 digits, this will +// parse an ASCII string. +// +// store_spans is a *runtime* flag (not a template parameter, deliberately: a +// template would create a second instantiation of this whole function and the +// extra icache pressure wipes out the gain). When false, the integer/fraction +// spans (read only by the rare digit_comp slow path) are not materialized, +// which keeps the fat parsed_number_string_t off the hot path. The caller +// re-parses with store_spans=true if the slow path is actually reached. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t +parse_number_string_impl(UC const *p, UC const *pend, + parse_options_t options, + bool store_spans) noexcept { + chars_format const fmt = detail::adjust_for_feature_macros(options.format); + UC const decimal_point = options.decimal_point; + + parsed_number_string_t answer; + answer.valid = false; + answer.too_many_digits = false; + // assume p < pend, so dereference without checks; + answer.negative = (*p == UC('-')); + // C++17 20.19.3.(7.1) explicitly forbids '+' sign here + if ((*p == UC('-')) || (uint64_t(fmt & chars_format::allow_leading_plus) && + !basic_json_fmt && *p == UC('+'))) { + ++p; + if (p == pend) { + return report_parse_error( + p, parse_error::missing_integer_or_dot_after_sign); + } + FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) { + if (!is_integer(*p)) { // a sign must be followed by an integer + return report_parse_error(p, + parse_error::missing_integer_after_sign); + } + } + else { + if (!is_integer(*p) && + (*p != + decimal_point)) { // a sign must be followed by an integer or the dot + return report_parse_error( + p, parse_error::missing_integer_or_dot_after_sign); + } + } + } + UC const *const start_digits = p; + + uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad) + + // Straight-line unroll of the integer-part scan: most integer parts are + // 1-5 digits, so peeling the first iterations eliminates the loop back-edge + // for the common case. Semantics are identical to the original `while` loop: + // i = 10*i + digit, advancing p. + if ((p != pend) && is_integer(*p)) { + i = uint64_t(*p - UC('0')); + ++p; + if ((p != pend) && is_integer(*p)) { + i = 10 * i + uint64_t(*p - UC('0')); + ++p; + if ((p != pend) && is_integer(*p)) { + i = 10 * i + uint64_t(*p - UC('0')); + ++p; + if ((p != pend) && is_integer(*p)) { + i = 10 * i + uint64_t(*p - UC('0')); + ++p; + if ((p != pend) && is_integer(*p)) { + i = 10 * i + uint64_t(*p - UC('0')); + ++p; + while ((p != pend) && is_integer(*p)) { + // a multiplication by 10 is cheaper than an arbitrary integer + // multiplication + i = 10 * i + + uint64_t(*p - UC('0')); // might overflow, handled later + ++p; + } + } + } + } + } + } + UC const *const end_of_integer_part = p; + int64_t digit_count = int64_t(end_of_integer_part - start_digits); + if (store_spans) { + answer.integer = span(start_digits, size_t(digit_count)); + } + FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) { + // at least 1 digit in integer part, without leading zeros + if (digit_count == 0) { + return report_parse_error(p, parse_error::no_digits_in_integer_part); + } + if ((start_digits[0] == UC('0') && digit_count > 1)) { + return report_parse_error(start_digits, + parse_error::leading_zeros_in_integer_part); + } + } + else FASTFLOAT_IF_CONSTEXPR17(basic_javascript_fmt) { + // ECMAScript DecimalIntegerLiteral is "0" or a non-zero digit followed by + // digits: no leading zeros. Unlike JSON, the integer part may be empty + // (".5"); the no_digits_in_mantissa check below still rejects ".". + if ((digit_count > 1) && (start_digits[0] == UC('0'))) { + if (!uint64_t(fmt & detail::basic_javascript_sloppy_fmt)) { + return report_parse_error( + start_digits, parse_error::leading_zeros_in_integer_part); + } + // Sloppy mode (Annex B): a NonOctalDecimalIntegerLiteral has a leading + // zero and at least one digit that is 8 or 9 ("08.5" is 8.5). With + // octal digits only, it is a LegacyOctalIntegerLiteral ("0775"), which + // is not a decimal number: report it so the caller can parse it in + // base 8. + bool has_non_octal_digit = false; + for (UC const *q = start_digits; q != end_of_integer_part; ++q) { + has_non_octal_digit |= (*q >= UC('8')); + } + if (!has_non_octal_digit) { + return report_parse_error(start_digits, + parse_error::legacy_octal_integer_part); + } + } + } + + int64_t exponent = 0; + bool const has_decimal_point = (p != pend) && (*p == decimal_point); + if (has_decimal_point) { + ++p; + UC const *before = p; + // can occur at most twice without overflowing, but let it occur more, since + // for integers with many digits, digit parsing is the primary bottleneck. + loop_parse_if_eight_digits(p, pend, i); + + while ((p != pend) && is_integer(*p)) { + uint8_t digit = uint8_t(*p - UC('0')); + ++p; + i = i * 10 + digit; // in rare cases, this will overflow, but that's ok + } + exponent = before - p; + if (store_spans) { + answer.fraction = span(before, size_t(p - before)); + } + digit_count -= exponent; + } + FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) { + // at least 1 digit in fractional part + if (has_decimal_point && exponent == 0) { + return report_parse_error(p, + parse_error::no_digits_in_fractional_part); + } + } + else if (digit_count == 0) { // we must have encountered at least one integer! + return report_parse_error(p, parse_error::no_digits_in_mantissa); + } + int64_t exp_number = 0; // explicit exponential part + if ((uint64_t(fmt & chars_format::scientific) && (p != pend) && + ((UC('e') == *p) || (UC('E') == *p))) || + (uint64_t(fmt & detail::basic_fortran_fmt) && (p != pend) && + ((UC('+') == *p) || (UC('-') == *p) || (UC('d') == *p) || + (UC('D') == *p)))) { + UC const *location_of_e = p; + if ((UC('e') == *p) || (UC('E') == *p) || (UC('d') == *p) || + (UC('D') == *p)) { + ++p; + } + bool neg_exp = false; + if ((p != pend) && (UC('-') == *p)) { + neg_exp = true; + ++p; + } else if ((p != pend) && + (UC('+') == + *p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1) + ++p; + } + if ((p == pend) || !is_integer(*p)) { + if (!uint64_t(fmt & chars_format::fixed)) { + // The exponential part is invalid for scientific notation, so it must + // be a trailing token for fixed notation. However, fixed notation is + // disabled, so report a scientific notation error. + return report_parse_error(p, parse_error::missing_exponential_part); + } + // Otherwise, we will be ignoring the 'e'. + p = location_of_e; + } else { + while ((p != pend) && is_integer(*p)) { + uint8_t digit = uint8_t(*p - UC('0')); + if (exp_number < 0x10000000) { + exp_number = 10 * exp_number + digit; + } + ++p; + } + if (neg_exp) { + exp_number = -exp_number; + } + exponent += exp_number; + } + } else { + // If it scientific and not fixed, we have to bail out. + if (uint64_t(fmt & chars_format::scientific) && + !uint64_t(fmt & chars_format::fixed)) { + return report_parse_error(p, parse_error::missing_exponential_part); + } + } + answer.lastmatch = p; + answer.valid = true; + + // If we frequently had to deal with long strings of digits, + // we could extend our code by using a 128-bit integer instead + // of a 64-bit integer. However, this is uncommon. + // + // We can deal with up to 19 digits. + if (digit_count > 19) { // this is uncommon + // It is possible that the integer had an overflow. + // We have to handle the case where we have 0.0000somenumber. + // We need to be mindful of the case where we only have zeroes... + // E.g., 0.000000000...000. + UC const *start = start_digits; + while ((start != pend) && (*start == UC('0') || *start == decimal_point)) { + if (*start == UC('0')) { + digit_count--; + } + start++; + } + + if (digit_count > 19) { + answer.too_many_digits = true; + // The truncation recompute below reads the integer/fraction spans. When + // store_spans is false we didn't materialize them, so just flag + // too_many_digits; the caller re-parses with store_spans=true to obtain + // the corrected mantissa/exponent before taking the slow path. + if (store_spans) { + // Let us start again, this time, avoiding overflows. + // We don't need to call if is_integer, since we use the + // pre-tokenized spans from above. + i = 0; + p = answer.integer.ptr; + UC const *int_end = p + answer.integer.len(); + uint64_t const minimal_nineteen_digit_integer{1000000000000000000}; + while ((i < minimal_nineteen_digit_integer) && (p != int_end)) { + i = i * 10 + uint64_t(*p - UC('0')); + ++p; + } + if (i >= minimal_nineteen_digit_integer) { // We have a big integer + exponent = end_of_integer_part - p + exp_number; + } else { // We have a value with a fractional component. + p = answer.fraction.ptr; + UC const *frac_end = p + answer.fraction.len(); + while ((i < minimal_nineteen_digit_integer) && (p != frac_end)) { + i = i * 10 + uint64_t(*p - UC('0')); + ++p; + } + exponent = answer.fraction.ptr - p + exp_number; + } + // We have now corrected both exponent and i, to a truncated value + } + } + } + answer.exponent = exponent; + answer.mantissa = i; + return answer; +} + +// Cold instantiation of the parser: the ECMAScript integer-part rule costs two +// error returns plus the Annex B octal scan, and parse_number_string_impl is +// force-inlined, so an inlined javascript body would enlarge the frame of every +// caller that never asks for it. Out of line, it costs those callers nothing. +template +fastfloat_never_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t +parse_number_string_javascript(UC const *p, UC const *pend, + parse_options_t options, + bool store_spans) noexcept { + return parse_number_string_impl(p, pend, options, + store_spans); +} + +// Public entry point, behaviour unchanged: chars_format::javascript is still +// honoured, it is just selected here once instead of being re-tested inside the +// parser loop. Callers that have already ruled the format out (see +// from_chars_float_advanced) should call parse_number_string_impl directly so +// that not even this test reaches their hot path. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t +parse_number_string(UC const *p, UC const *pend, parse_options_t options, + bool store_spans = true) noexcept { + // JSON and JavaScript are mutually exclusive, so only the non-JSON + // instantiation has to look at the flag. + FASTFLOAT_IF_CONSTEXPR17(!basic_json_fmt) { + if fastfloat_unlikely (uint64_t(options.format & + detail::basic_javascript_fmt)) { + return parse_number_string_javascript(p, pend, options, store_spans); + } + } + return parse_number_string_impl(p, pend, options, + store_spans); +} + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 from_chars_result_t +parse_int_string(UC const *p, UC const *pend, T &value, + parse_options_t options) { + chars_format const fmt = detail::adjust_for_feature_macros(options.format); + int const base = options.base; + + from_chars_result_t answer; + + UC const *const first = p; + + bool const negative = (*p == UC('-')); +#ifdef FASTFLOAT_VISUAL_STUDIO +#pragma warning(push) +#pragma warning(disable : 4127) +#endif + if (!std::is_signed::value && negative) { +#ifdef FASTFLOAT_VISUAL_STUDIO +#pragma warning(pop) +#endif + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + return answer; + } + if ((*p == UC('-')) || + (uint64_t(fmt & chars_format::allow_leading_plus) && (*p == UC('+')))) { + ++p; + } + + UC const *const start_num = p; + + while (p != pend && *p == UC('0')) { + ++p; + } + + bool const has_leading_zeros = p > start_num; + + UC const *const start_digits = p; + + FASTFLOAT_IF_CONSTEXPR17( + (std::is_same::value && sizeof(UC) == 1)) { + if (base == 10) { + const size_t len = static_cast(pend - p); + if (len == 0) { + if (has_leading_zeros) { + value = 0; + answer.ec = std::errc(); + answer.ptr = p; + } else { + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + } + return answer; + } + + uint32_t digits; + +#if FASTFLOAT_HAS_IS_CONSTANT_EVALUATED && FASTFLOAT_HAS_BIT_CAST + if (std::is_constant_evaluated()) { + uint8_t str[4]{}; + for (size_t j = 0; j < 4 && j < len; ++j) { + str[j] = static_cast(p[j]); + } + digits = std::bit_cast(str); +#if FASTFLOAT_IS_BIG_ENDIAN + digits = byteswap_32(digits); +#endif + } +#else + if (false) { + } +#endif + else if (len >= 4) { + ::memcpy(&digits, p, 4); +#if FASTFLOAT_IS_BIG_ENDIAN + digits = byteswap_32(digits); +#endif + } else { + uint32_t b0 = static_cast(p[0]); + uint32_t b1 = (len > 1) ? static_cast(p[1]) : 0xFFu; + uint32_t b2 = (len > 2) ? static_cast(p[2]) : 0xFFu; + uint32_t b3 = 0xFFu; + digits = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); + } + + uint32_t magic = + ((digits + 0x46464646u) | (digits - 0x30303030u)) & 0x80808080u; + uint32_t tz = + static_cast(countr_zero_32(magic)); // 7, 15, 23, 31, or 32 + uint32_t nd = (tz == 32) ? 4 : (tz >> 3); + nd = static_cast(nd < len ? nd : len); + if (nd == 0) { + if (has_leading_zeros) { + value = 0; + answer.ec = std::errc(); + answer.ptr = p; + return answer; + } + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + return answer; + } + if (nd > 3) { + const UC *q = p + nd; + size_t rem = len - nd; + while (rem) { + if (*q < UC('0') || *q > UC('9')) + break; + ++q; + --rem; + } + answer.ec = std::errc::result_out_of_range; + answer.ptr = q; + return answer; + } + + digits ^= 0x30303030u; + digits <<= ((4 - nd) * 8); + + uint32_t check = ((digits >> 24) & 0xff) | ((digits >> 8) & 0xff00) | + ((digits << 8) & 0xff0000); + if (check > 0x00020505) { + answer.ec = std::errc::result_out_of_range; + answer.ptr = p + nd; + return answer; + } + value = static_cast((0x640a01 * digits) >> 24); + answer.ec = std::errc(); + answer.ptr = p + nd; + return answer; + } + } + + FASTFLOAT_IF_CONSTEXPR17( + (std::is_same::value && sizeof(UC) == 1)) { + if (base == 10) { + const size_t len = size_t(pend - p); + if (len == 0) { + if (has_leading_zeros) { + value = 0; + answer.ec = std::errc(); + answer.ptr = p; + } else { + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + } + return answer; + } + + if (len >= 4) { + uint32_t digits = read4_to_u32(p); + if (is_made_of_four_digits_fast(digits)) { + uint32_t v = parse_four_digits_unrolled(digits); + if (len >= 5 && is_integer(p[4])) { + v = v * 10 + uint32_t(p[4] - '0'); + if (len >= 6 && is_integer(p[5])) { + answer.ec = std::errc::result_out_of_range; + const UC *q = p + 5; + while (q != pend && is_integer(*q)) { + q++; + } + answer.ptr = q; + return answer; + } + if (v > 65535) { + answer.ec = std::errc::result_out_of_range; + answer.ptr = p + 5; + return answer; + } + value = uint16_t(v); + answer.ec = std::errc(); + answer.ptr = p + 5; + return answer; + } + // 4 digits + value = uint16_t(v); + answer.ec = std::errc(); + answer.ptr = p + 4; + return answer; + } + } + } + } + + uint64_t i = 0; + if (base == 10) { + loop_parse_if_eight_digits(p, pend, i); // use SIMD if possible + } + while (p != pend) { + uint8_t digit = ch_to_digit(*p); + if (digit >= base) { + break; + } + i = uint64_t(base) * i + digit; // might overflow, check this later + p++; + } + + size_t digit_count = size_t(p - start_digits); + + if (digit_count == 0) { + if (has_leading_zeros) { + value = 0; + answer.ec = std::errc(); + answer.ptr = p; + } else { + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + } + return answer; + } + + answer.ptr = p; + + // check u64 overflow + size_t max_digits = max_digits_u64(base); + if (digit_count > max_digits) { + answer.ec = std::errc::result_out_of_range; + return answer; + } + // this check can be eliminated for all other types, but they will all require + // a max_digits(base) equivalent + if (digit_count == max_digits) { + // At the max_digits boundary the accumulator `i` may have wrapped around + // 2^64. A plain `i < min_safe_u64(base)` test is not sufficient: for any + // base whose max_digits-length range exceeds 2^64 (base 10 reaches + // ~5.4 * 2^64 at 20 digits) the value can wrap a whole multiple of 2^64 and + // land back above min_safe, slipping through. Decide exactly in O(1) using + // the leading digit, following the approach used in simdjson: + // ms == min_safe_u64(base) == base^(max_digits-1), the smallest + // max_digits-length value. + // dmax == the largest leading digit whose number can still fit in u64. + // The leading-digit band [d*ms, (d+1)*ms) has width ms < 2^64, so within + // the single band where d == dmax the value straddles 2^64 at most once, + // and a single threshold separates wrapped from non-wrapped values. A + // leading digit above dmax always overflows; below dmax always fits. + uint64_t const ms = min_safe_u64(base); + uint64_t const dmax = (std::numeric_limits::max)() / ms; + uint64_t const lead = ch_to_digit(*start_digits); + if (lead > dmax || (lead == dmax && i < dmax * ms)) { + answer.ec = std::errc::result_out_of_range; + return answer; + } + } + + // check other types overflow + if (!std::is_same::value) { + if (i > uint64_t((std::numeric_limits::max)()) + uint64_t(negative)) { + answer.ec = std::errc::result_out_of_range; + return answer; + } + } + + if (negative) { +#ifdef FASTFLOAT_VISUAL_STUDIO +#pragma warning(push) +#pragma warning(disable : 4146) +#endif + // this weird workaround is required because: + // - converting unsigned to signed when its value is greater than signed max + // is UB pre-C++23. + // - reinterpret_casting (~i + 1) would work, but it is not constexpr + // this is always optimized into a neg instruction (note: T is an integer + // type) + value = T(-(std::numeric_limits::max)() - + T(i - uint64_t((std::numeric_limits::max)()))); +#ifdef FASTFLOAT_VISUAL_STUDIO +#pragma warning(pop) +#endif + } else { + value = T(i); + } + + answer.ec = std::errc(); + return answer; +} + +} // namespace fast_float + +#endif diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/bigint.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/bigint.h new file mode 100644 index 0000000000..665debb721 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/bigint.h @@ -0,0 +1,638 @@ +#ifndef FASTFLOAT_BIGINT_H +#define FASTFLOAT_BIGINT_H + +#include +#include +#include +#include + +#include "float_common.h" + +namespace fast_float { + +// the limb width: we want efficient multiplication of double the bits in +// limb, or for 64-bit limbs, at least 64-bit multiplication where we can +// extract the high and low parts efficiently. this is every 64-bit +// architecture except for sparc, which emulates 128-bit multiplication. +// we might have platforms where `CHAR_BIT` is not 8, so let's avoid +// doing `8 * sizeof(limb)`. +#if defined(FASTFLOAT_64BIT) && !defined(__sparc) +#define FASTFLOAT_64BIT_LIMB 1 +typedef uint64_t limb; +constexpr size_t limb_bits = 64; +#else +#define FASTFLOAT_32BIT_LIMB +typedef uint32_t limb; +constexpr size_t limb_bits = 32; +#endif + +typedef span limb_span; + +// number of bits in a bigint. this needs to be at least the number +// of bits required to store the largest bigint, which is +// `log2(10**(digits + max_exp))`, or `log2(10**(767 + 342))`, or +// ~3600 bits, so we round to 4000. +constexpr size_t bigint_bits = 4000; +constexpr size_t bigint_limbs = bigint_bits / limb_bits; + +// vector-like type that is allocated on the stack. the entire +// buffer is pre-allocated, and only the length changes. +template struct stackvec { + limb data[size]; + // we never need more than 150 limbs + uint16_t length{0}; + + stackvec() = default; + stackvec(stackvec const &) = delete; + stackvec &operator=(stackvec const &) = delete; + stackvec(stackvec &&) = delete; + stackvec &operator=(stackvec &&other) = delete; + + // create stack vector from existing limb span. + FASTFLOAT_CONSTEXPR20 stackvec(limb_span s) { + FASTFLOAT_ASSERT(try_extend(s)); + } + + FASTFLOAT_CONSTEXPR14 limb &operator[](size_t index) noexcept { + FASTFLOAT_DEBUG_ASSERT(index < length); + return data[index]; + } + + FASTFLOAT_CONSTEXPR14 const limb &operator[](size_t index) const noexcept { + FASTFLOAT_DEBUG_ASSERT(index < length); + return data[index]; + } + + // index from the end of the container + FASTFLOAT_CONSTEXPR14 const limb &rindex(size_t index) const noexcept { + FASTFLOAT_DEBUG_ASSERT(index < length); + size_t rindex = length - index - 1; + return data[rindex]; + } + + // set the length, without bounds checking. + FASTFLOAT_CONSTEXPR14 void set_len(size_t len) noexcept { + length = uint16_t(len); + } + + constexpr size_t len() const noexcept { return length; } + + constexpr bool is_empty() const noexcept { return length == 0; } + + constexpr size_t capacity() const noexcept { return size; } + + // append item to vector, without bounds checking + FASTFLOAT_CONSTEXPR14 void push_unchecked(limb value) noexcept { + data[length] = value; + length++; + } + + // append item to vector, returning if item was added + FASTFLOAT_CONSTEXPR14 bool try_push(limb value) noexcept { + if (len() < capacity()) { + push_unchecked(value); + return true; + } else { + return false; + } + } + + // add items to the vector, from a span, without bounds checking + FASTFLOAT_CONSTEXPR20 void extend_unchecked(limb_span s) noexcept { + limb *ptr = data + length; + std::copy_n(s.ptr, s.len(), ptr); + set_len(len() + s.len()); + } + + // try to add items to the vector, returning if items were added + FASTFLOAT_CONSTEXPR20 bool try_extend(limb_span s) noexcept { + if (len() + s.len() <= capacity()) { + extend_unchecked(s); + return true; + } else { + return false; + } + } + + // resize the vector, without bounds checking + // if the new size is longer than the vector, assign value to each + // appended item. + FASTFLOAT_CONSTEXPR20 + void resize_unchecked(size_t new_len, limb value) noexcept { + if (new_len > len()) { + size_t count = new_len - len(); + limb *first = data + len(); + limb *last = first + count; + ::std::fill(first, last, value); + set_len(new_len); + } else { + set_len(new_len); + } + } + + // try to resize the vector, returning if the vector was resized. + FASTFLOAT_CONSTEXPR20 bool try_resize(size_t new_len, limb value) noexcept { + if (new_len > capacity()) { + return false; + } else { + resize_unchecked(new_len, value); + return true; + } + } + + // check if any limbs are non-zero after the given index. + // this needs to be done in reverse order, since the index + // is relative to the most significant limbs. + FASTFLOAT_CONSTEXPR14 bool nonzero(size_t index) const noexcept { + while (index < len()) { + if (rindex(index) != 0) { + return true; + } + index++; + } + return false; + } + + // normalize the big integer, so most-significant zero limbs are removed. + FASTFLOAT_CONSTEXPR14 void normalize() noexcept { + while (len() > 0 && rindex(0) == 0) { + length--; + } + } +}; + +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 uint64_t +empty_hi64(bool &truncated) noexcept { + truncated = false; + return 0; +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 uint64_t +uint64_hi64(uint64_t r0, bool &truncated) noexcept { + truncated = false; + int shl = leading_zeroes(r0); + return r0 << shl; +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 uint64_t +uint64_hi64(uint64_t r0, uint64_t r1, bool &truncated) noexcept { + int shl = leading_zeroes(r0); + if (shl == 0) { + truncated = r1 != 0; + return r0; + } else { + int shr = 64 - shl; + truncated = (r1 << shl) != 0; + return (r0 << shl) | (r1 >> shr); + } +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 uint64_t +uint32_hi64(uint32_t r0, bool &truncated) noexcept { + return uint64_hi64(r0, truncated); +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 uint64_t +uint32_hi64(uint32_t r0, uint32_t r1, bool &truncated) noexcept { + uint64_t x0 = r0; + uint64_t x1 = r1; + return uint64_hi64((x0 << 32) | x1, truncated); +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 uint64_t +uint32_hi64(uint32_t r0, uint32_t r1, uint32_t r2, bool &truncated) noexcept { + uint64_t x0 = r0; + uint64_t x1 = r1; + uint64_t x2 = r2; + return uint64_hi64(x0, (x1 << 32) | x2, truncated); +} + +// add two small integers, checking for overflow. +// we want an efficient operation. for msvc, where +// we don't have built-in intrinsics, this is still +// pretty fast. +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 limb +scalar_add(limb x, limb y, bool &overflow) noexcept { + limb z; +// gcc and clang +#if defined(__has_builtin) +#if __has_builtin(__builtin_add_overflow) + if (!cpp20_and_in_constexpr()) { + overflow = __builtin_add_overflow(x, y, &z); + return z; + } +#endif +#endif + + // generic, this still optimizes correctly on MSVC. + z = x + y; + overflow = z < x; + return z; +} + +// multiply two small integers, getting both the high and low bits. +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 limb +scalar_mul(limb x, limb y, limb &carry) noexcept { +#ifdef FASTFLOAT_64BIT_LIMB +#if defined(__SIZEOF_INT128__) + // GCC and clang both define it as an extension. + __uint128_t z = __uint128_t(x) * __uint128_t(y) + __uint128_t(carry); + carry = limb(z >> limb_bits); + return limb(z); +#else + // fallback, no native 128-bit integer multiplication with carry. + // on msvc, this optimizes identically, somehow. + value128 z = full_multiplication(x, y); + bool overflow; + z.low = scalar_add(z.low, carry, overflow); + z.high += uint64_t(overflow); // cannot overflow + carry = z.high; + return z.low; +#endif +#else + uint64_t z = uint64_t(x) * uint64_t(y) + uint64_t(carry); + carry = limb(z >> limb_bits); + return limb(z); +#endif +} + +// add scalar value to bigint starting from offset. +// used in grade school multiplication +template +inline FASTFLOAT_CONSTEXPR20 bool small_add_from(stackvec &vec, limb y, + size_t start) noexcept { + size_t index = start; + limb carry = y; + bool overflow; + while (carry != 0 && index < vec.len()) { + vec[index] = scalar_add(vec[index], carry, overflow); + carry = limb(overflow); + index += 1; + } + if (carry != 0) { + FASTFLOAT_TRY(vec.try_push(carry)); + } + return true; +} + +// add scalar value to bigint. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool +small_add(stackvec &vec, limb y) noexcept { + return small_add_from(vec, y, 0); +} + +// multiply bigint by scalar value. +template +inline FASTFLOAT_CONSTEXPR20 bool small_mul(stackvec &vec, + limb y) noexcept { + limb carry = 0; + for (size_t index = 0; index < vec.len(); index++) { + vec[index] = scalar_mul(vec[index], y, carry); + } + if (carry != 0) { + FASTFLOAT_TRY(vec.try_push(carry)); + } + return true; +} + +// add bigint to bigint starting from index. +// used in grade school multiplication +template +FASTFLOAT_CONSTEXPR20 bool large_add_from(stackvec &x, limb_span y, + size_t start) noexcept { + // the effective x buffer is from `xstart..x.len()`, so exit early + // if we can't get that current range. + if (x.len() < start || y.len() > x.len() - start) { + FASTFLOAT_TRY(x.try_resize(y.len() + start, 0)); + } + + bool carry = false; + for (size_t index = 0; index < y.len(); index++) { + limb xi = x[index + start]; + limb yi = y[index]; + bool c1 = false; + bool c2 = false; + xi = scalar_add(xi, yi, c1); + if (carry) { + xi = scalar_add(xi, 1, c2); + } + x[index + start] = xi; + carry = c1 | c2; + } + + // handle overflow + if (carry) { + FASTFLOAT_TRY(small_add_from(x, 1, y.len() + start)); + } + return true; +} + +// add bigint to bigint. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool +large_add_from(stackvec &x, limb_span y) noexcept { + return large_add_from(x, y, 0); +} + +// grade-school multiplication algorithm +template +FASTFLOAT_CONSTEXPR20 bool long_mul(stackvec &x, limb_span y) noexcept { + limb_span xs = limb_span(x.data, x.len()); + stackvec z(xs); + limb_span zs = limb_span(z.data, z.len()); + + if (y.len() != 0) { + limb y0 = y[0]; + FASTFLOAT_TRY(small_mul(x, y0)); + for (size_t index = 1; index < y.len(); index++) { + limb yi = y[index]; + stackvec zi; + if (yi != 0) { + // re-use the same buffer throughout + zi.set_len(0); + FASTFLOAT_TRY(zi.try_extend(zs)); + FASTFLOAT_TRY(small_mul(zi, yi)); + limb_span zis = limb_span(zi.data, zi.len()); + FASTFLOAT_TRY(large_add_from(x, zis, index)); + } + } + } + + x.normalize(); + return true; +} + +// grade-school multiplication algorithm +template +FASTFLOAT_CONSTEXPR20 bool large_mul(stackvec &x, limb_span y) noexcept { + if (y.len() == 1) { + FASTFLOAT_TRY(small_mul(x, y[0])); + } else { + FASTFLOAT_TRY(long_mul(x, y)); + } + return true; +} + +template struct pow5_tables { + static constexpr uint32_t large_step = 135; + static constexpr uint64_t small_power_of_5[] = { + 1UL, + 5UL, + 25UL, + 125UL, + 625UL, + 3125UL, + 15625UL, + 78125UL, + 390625UL, + 1953125UL, + 9765625UL, + 48828125UL, + 244140625UL, + 1220703125UL, + 6103515625UL, + 30517578125UL, + 152587890625UL, + 762939453125UL, + 3814697265625UL, + 19073486328125UL, + 95367431640625UL, + 476837158203125UL, + 2384185791015625UL, + 11920928955078125UL, + 59604644775390625UL, + 298023223876953125UL, + 1490116119384765625UL, + 7450580596923828125UL, + }; +#ifdef FASTFLOAT_64BIT_LIMB + constexpr static limb large_power_of_5[] = { + 1414648277510068013UL, 9180637584431281687UL, 4539964771860779200UL, + 10482974169319127550UL, 198276706040285095UL}; +#else + constexpr static limb large_power_of_5[] = { + 4279965485U, 329373468U, 4020270615U, 2137533757U, 4287402176U, + 1057042919U, 1071430142U, 2440757623U, 381945767U, 46164893U}; +#endif +}; + +#if FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE + +template constexpr uint32_t pow5_tables::large_step; + +template constexpr uint64_t pow5_tables::small_power_of_5[]; + +template constexpr limb pow5_tables::large_power_of_5[]; + +#endif + +// big integer type. implements a small subset of big integer +// arithmetic, using simple algorithms since asymptotically +// faster algorithms are slower for a small number of limbs. +// all operations assume the big-integer is normalized. +struct bigint : pow5_tables<> { + // storage of the limbs, in little-endian order. + stackvec vec; + + FASTFLOAT_CONSTEXPR20 bigint() : vec() {} + + bigint(bigint const &) = delete; + bigint &operator=(bigint const &) = delete; + bigint(bigint &&) = delete; + bigint &operator=(bigint &&other) = delete; + + FASTFLOAT_CONSTEXPR20 bigint(uint64_t value) : vec() { +#ifdef FASTFLOAT_64BIT_LIMB + vec.push_unchecked(value); +#else + vec.push_unchecked(uint32_t(value)); + vec.push_unchecked(uint32_t(value >> 32)); +#endif + vec.normalize(); + } + + // get the high 64 bits from the vector, and if bits were truncated. + // this is to get the significant digits for the float. + FASTFLOAT_CONSTEXPR20 uint64_t hi64(bool &truncated) const noexcept { +#ifdef FASTFLOAT_64BIT_LIMB + if (vec.len() == 0) { + return empty_hi64(truncated); + } else if (vec.len() == 1) { + return uint64_hi64(vec.rindex(0), truncated); + } else { + uint64_t result = uint64_hi64(vec.rindex(0), vec.rindex(1), truncated); + truncated |= vec.nonzero(2); + return result; + } +#else + if (vec.len() == 0) { + return empty_hi64(truncated); + } else if (vec.len() == 1) { + return uint32_hi64(vec.rindex(0), truncated); + } else if (vec.len() == 2) { + return uint32_hi64(vec.rindex(0), vec.rindex(1), truncated); + } else { + uint64_t result = + uint32_hi64(vec.rindex(0), vec.rindex(1), vec.rindex(2), truncated); + truncated |= vec.nonzero(3); + return result; + } +#endif + } + + // compare two big integers, returning the large value. + // assumes both are normalized. if the return value is + // negative, other is larger, if the return value is + // positive, this is larger, otherwise they are equal. + // the limbs are stored in little-endian order, so we + // must compare the limbs in ever order. + FASTFLOAT_CONSTEXPR20 int compare(bigint const &other) const noexcept { + if (vec.len() > other.vec.len()) { + return 1; + } else if (vec.len() < other.vec.len()) { + return -1; + } else { + for (size_t index = vec.len(); index > 0; index--) { + limb xi = vec[index - 1]; + limb yi = other.vec[index - 1]; + if (xi > yi) { + return 1; + } else if (xi < yi) { + return -1; + } + } + return 0; + } + } + + // shift left each limb n bits, carrying over to the new limb + // returns true if we were able to shift all the digits. + FASTFLOAT_CONSTEXPR20 bool shl_bits(size_t n) noexcept { + // Internally, for each item, we shift left by n, and add the previous + // right shifted limb-bits. + // For example, we transform (for u8) shifted left 2, to: + // b10100100 b01000010 + // b10 b10010001 b00001000 + FASTFLOAT_DEBUG_ASSERT(n != 0); + FASTFLOAT_DEBUG_ASSERT(n < sizeof(limb) * 8); + + size_t shl = n; + size_t shr = limb_bits - shl; + limb prev = 0; + for (size_t index = 0; index < vec.len(); index++) { + limb xi = vec[index]; + vec[index] = (xi << shl) | (prev >> shr); + prev = xi; + } + + limb carry = prev >> shr; + if (carry != 0) { + return vec.try_push(carry); + } + return true; + } + + // move the limbs left by `n` limbs. + FASTFLOAT_CONSTEXPR20 bool shl_limbs(size_t n) noexcept { + FASTFLOAT_DEBUG_ASSERT(n != 0); + if (n + vec.len() > vec.capacity()) { + return false; + } else if (!vec.is_empty()) { + // move limbs + limb *dst = vec.data + n; + limb const *src = vec.data; + std::copy_backward(src, src + vec.len(), dst + vec.len()); + // fill in empty limbs + limb *first = vec.data; + limb *last = first + n; + ::std::fill(first, last, 0); + vec.set_len(n + vec.len()); + return true; + } else { + return true; + } + } + + // move the limbs left by `n` bits. + FASTFLOAT_CONSTEXPR20 bool shl(size_t n) noexcept { + size_t rem = n % limb_bits; + size_t div = n / limb_bits; + if (rem != 0) { + FASTFLOAT_TRY(shl_bits(rem)); + } + if (div != 0) { + FASTFLOAT_TRY(shl_limbs(div)); + } + return true; + } + + // get the number of leading zeros in the bigint. + FASTFLOAT_CONSTEXPR20 int ctlz() const noexcept { + if (vec.is_empty()) { + return 0; + } else { +#ifdef FASTFLOAT_64BIT_LIMB + return leading_zeroes(vec.rindex(0)); +#else + // no use defining a specialized leading_zeroes for a 32-bit type. + uint64_t r0 = vec.rindex(0); + return leading_zeroes(r0 << 32); +#endif + } + } + + // get the number of bits in the bigint. + FASTFLOAT_CONSTEXPR20 int bit_length() const noexcept { + int lz = ctlz(); + return int(limb_bits * vec.len()) - lz; + } + + FASTFLOAT_CONSTEXPR20 bool mul(limb y) noexcept { return small_mul(vec, y); } + + FASTFLOAT_CONSTEXPR20 bool add(limb y) noexcept { return small_add(vec, y); } + + // multiply as if by 2 raised to a power. + FASTFLOAT_CONSTEXPR20 bool pow2(uint32_t exp) noexcept { return shl(exp); } + + // multiply as if by 5 raised to a power. + FASTFLOAT_CONSTEXPR20 bool pow5(uint32_t exp) noexcept { + // multiply by a power of 5 + size_t large_length = sizeof(large_power_of_5) / sizeof(limb); + limb_span large = limb_span(large_power_of_5, large_length); + while (exp >= large_step) { + FASTFLOAT_TRY(large_mul(vec, large)); + exp -= large_step; + } +#ifdef FASTFLOAT_64BIT_LIMB + uint32_t small_step = 27; + limb max_native = 7450580596923828125UL; +#else + uint32_t small_step = 13; + limb max_native = 1220703125U; +#endif + while (exp >= small_step) { + FASTFLOAT_TRY(small_mul(vec, max_native)); + exp -= small_step; + } + if (exp != 0) { + // Work around clang bug https://godbolt.org/z/zedh7rrhc + // This is similar to https://github.com/llvm/llvm-project/issues/47746, + // except the workaround described there don't work here + FASTFLOAT_TRY(small_mul(vec, limb((static_cast(small_power_of_5[0]), + small_power_of_5[exp])))); + } + + return true; + } + + // multiply as if by 10 raised to a power. + FASTFLOAT_CONSTEXPR20 bool pow10(uint32_t exp) noexcept { + FASTFLOAT_TRY(pow5(exp)); + return pow2(exp); + } +}; + +} // namespace fast_float + +#endif diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/constexpr_feature_detect.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/constexpr_feature_detect.h new file mode 100644 index 0000000000..6751afecd6 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/constexpr_feature_detect.h @@ -0,0 +1,53 @@ +#ifndef FASTFLOAT_CONSTEXPR_FEATURE_DETECT_H +#define FASTFLOAT_CONSTEXPR_FEATURE_DETECT_H + +#ifdef __has_include +#if __has_include() +#include +#endif +#endif + +// Testing for https://wg21.link/N3652, adopted in C++14 +#if defined(__cpp_constexpr) && __cpp_constexpr >= 201304 +#define FASTFLOAT_CONSTEXPR14 constexpr +#else +#define FASTFLOAT_CONSTEXPR14 +#endif + +#if defined(__cpp_lib_bit_cast) && __cpp_lib_bit_cast >= 201806L +#define FASTFLOAT_HAS_BIT_CAST 1 +#else +#define FASTFLOAT_HAS_BIT_CAST 0 +#endif + +#if defined(__cpp_lib_is_constant_evaluated) && \ + __cpp_lib_is_constant_evaluated >= 201811L +#define FASTFLOAT_HAS_IS_CONSTANT_EVALUATED 1 +#else +#define FASTFLOAT_HAS_IS_CONSTANT_EVALUATED 0 +#endif + +#if defined(__cpp_if_constexpr) && __cpp_if_constexpr >= 201606L +#define FASTFLOAT_IF_CONSTEXPR17(x) if constexpr (x) +#else +#define FASTFLOAT_IF_CONSTEXPR17(x) if (x) +#endif + +// Testing for relevant C++20 constexpr library features +#if FASTFLOAT_HAS_IS_CONSTANT_EVALUATED && FASTFLOAT_HAS_BIT_CAST && \ + defined(__cpp_lib_constexpr_algorithms) && \ + __cpp_lib_constexpr_algorithms >= 201806L /*For std::copy and std::fill*/ +#define FASTFLOAT_CONSTEXPR20 constexpr +#define FASTFLOAT_IS_CONSTEXPR 1 +#else +#define FASTFLOAT_CONSTEXPR20 +#define FASTFLOAT_IS_CONSTEXPR 0 +#endif + +#if __cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) +#define FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE 0 +#else +#define FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE 1 +#endif + +#endif // FASTFLOAT_CONSTEXPR_FEATURE_DETECT_H diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/decimal_to_binary.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/decimal_to_binary.h new file mode 100644 index 0000000000..5389701b78 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/decimal_to_binary.h @@ -0,0 +1,238 @@ +#ifndef FASTFLOAT_DECIMAL_TO_BINARY_H +#define FASTFLOAT_DECIMAL_TO_BINARY_H + +#include "float_common.h" +#include "fast_table.h" +#include +#include +#include +#include +#include +#include + +namespace fast_float { + +// This will compute or rather approximate w * 5**q and return a pair of 64-bit +// words approximating the result, with the "high" part corresponding to the +// most significant bits and the low part corresponding to the least significant +// bits. +// +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 value128 +compute_product_approximation(int64_t q, uint64_t w) { + int const index = 2 * int(q - powers::smallest_power_of_five); + // For small values of q, e.g., q in [0,27], the answer is always exact + // because The line value128 firstproduct = full_multiplication(w, + // power_of_five_128[index]); gives the exact answer. + value128 firstproduct = + full_multiplication(w, powers::power_of_five_128[index]); + static_assert((bit_precision >= 0) && (bit_precision <= 64), + " precision should be in (0,64]"); + constexpr uint64_t precision_mask = + (bit_precision < 64) ? (uint64_t(0xFFFFFFFFFFFFFFFF) >> bit_precision) + : uint64_t(0xFFFFFFFFFFFFFFFF); + if ((firstproduct.high & precision_mask) == + precision_mask) { // could further guard with (lower + w < lower) + // regarding the second product, we only need secondproduct.high, but our + // expectation is that the compiler will optimize this extra work away if + // needed. + value128 secondproduct = + full_multiplication(w, powers::power_of_five_128[index + 1]); + firstproduct.low += secondproduct.high; + if (secondproduct.high > firstproduct.low) { + firstproduct.high++; + } + } + return firstproduct; +} + +namespace detail { +/** + * For q in (0,350), we have that + * f = (((152170 + 65536) * q ) >> 16); + * is equal to + * floor(p) + q + * where + * p = log(5**q)/log(2) = q * log(5)/log(2) + * + * For negative values of q in (-400,0), we have that + * f = (((152170 + 65536) * q ) >> 16); + * is equal to + * -ceil(p) + q + * where + * p = log(5**-q)/log(2) = -q * log(5)/log(2) + */ +constexpr fastfloat_really_inline int32_t power(int32_t q) noexcept { + return (((152170 + 65536) * q) >> 16) + 63; +} +} // namespace detail + +// create an adjusted mantissa, biased by the invalid power2 +// for significant digits already multiplied by 10 ** q. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 adjusted_mantissa +compute_error_scaled(int64_t q, uint64_t w, int lz) noexcept { + int hilz = int(w >> 63) ^ 1; + adjusted_mantissa answer; + answer.mantissa = w << hilz; + int bias = binary::mantissa_explicit_bits() - binary::minimum_exponent(); + answer.power2 = int32_t(detail::power(int32_t(q)) + bias - hilz - lz - 62 + + invalid_am_bias); + return answer; +} + +// w * 10 ** q, without rounding the representation up. +// the power2 in the exponent will be adjusted by invalid_am_bias. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 adjusted_mantissa +compute_error(int64_t q, uint64_t w) noexcept { + int lz = leading_zeroes(w); + w <<= lz; + value128 product = + compute_product_approximation(q, w); + return compute_error_scaled(q, product.high, lz); +} + +// Computers w * 10 ** q. +// The returned value should be a valid number that simply needs to be +// packed. However, in some very rare cases, the computation will fail. In such +// cases, we return an adjusted_mantissa with a negative power of 2: the caller +// should recompute in such cases. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 adjusted_mantissa +compute_float(int64_t q, uint64_t w) noexcept { + adjusted_mantissa answer; + if ((w == 0) || (q < binary::smallest_power_of_ten())) { + answer.power2 = 0; + answer.mantissa = 0; + // result should be zero + return answer; + } + if (q > binary::largest_power_of_ten()) { + // we want to get infinity: + answer.power2 = binary::infinite_power(); + answer.mantissa = 0; + return answer; + } + // At this point in time q is in [powers::smallest_power_of_five, + // powers::largest_power_of_five]. + + // We want the most significant bit of i to be 1. Shift if needed. + int lz = leading_zeroes(w); + w <<= lz; + + // The required precision is binary::mantissa_explicit_bits() + 3 because + // 1. We need the implicit bit + // 2. We need an extra bit for rounding purposes + // 3. We might lose a bit due to the "upperbit" routine (result too small, + // requiring a shift) + + value128 product = + compute_product_approximation(q, w); + // The computed 'product' is always sufficient. + // Mathematical proof: + // Noble Mushtak and Daniel Lemire, Fast Number Parsing Without Fallback (to + // appear) See script/mushtak_lemire.py + + // The "compute_product_approximation" function can be slightly slower than a + // branchless approach: value128 product = compute_product(q, w); but in + // practice, we can win big with the compute_product_approximation if its + // additional branch is easily predicted. Which is best is data specific. + int upperbit = int(product.high >> 63); + int shift = upperbit + 64 - binary::mantissa_explicit_bits() - 3; + + answer.mantissa = product.high >> shift; + + answer.power2 = int32_t(detail::power(int32_t(q)) + upperbit - lz - + binary::minimum_exponent()); + if (answer.power2 <= 0) { // we have a subnormal? + // Here have that answer.power2 <= 0 so -answer.power2 >= 0 + if (-answer.power2 + 1 >= + 64) { // if we have more than 64 bits below the minimum exponent, you + // have a zero for sure. + answer.power2 = 0; + answer.mantissa = 0; + // result should be zero + return answer; + } + // next line is safe because -answer.power2 + 1 < 64 + int const subnormal_shift = -answer.power2 + 1; + answer.mantissa >>= subnormal_shift; + // A subnormal result can also fall exactly between two floats. With at + // most 19 digits this never happens for float and double, but it does for + // std::float16_t (e.g., 2^-25 = 298023223876953125e-25), so we apply the + // same round-to-even test as in the normal case below. + // See script/format_parameters.py. + if (binary::subnormal_ties_possible() && (product.low <= 1) && + (q >= binary::min_exponent_round_to_even()) && + (q <= binary::max_exponent_round_to_even()) && + ((answer.mantissa & 3) == 1)) { + if (((answer.mantissa << subnormal_shift) << shift) == product.high) { + answer.mantissa &= ~uint64_t(1); // flip it so that we do not round up + } + } + answer.mantissa += (answer.mantissa & 1); // round up + answer.mantissa >>= 1; + // There is a weird scenario where we don't have a subnormal but just. + // Suppose we start with 2.2250738585072013e-308, we end up + // with 0x3fffffffffffff x 2^-1023-53 which is technically subnormal + // whereas 0x40000000000000 x 2^-1023-53 is normal. Now, we need to round + // up 0x3fffffffffffff x 2^-1023-53 and once we do, we are no longer + // subnormal, but we can only know this after rounding. + // So we only declare a subnormal if we are smaller than the threshold. + answer.power2 = + (answer.mantissa < (uint64_t(1) << binary::mantissa_explicit_bits())) + ? 0 + : 1; + return answer; + } + + // usually, we round *up*, but if we fall right in between and and we have an + // even basis, we need to round down + // We are only concerned with the cases where 5**q fits in single 64-bit word. + if ((product.low <= 1) && (q >= binary::min_exponent_round_to_even()) && + (q <= binary::max_exponent_round_to_even()) && + ((answer.mantissa & 3) == 1)) { // we may fall between two floats! + // To be in-between two floats we need that in doing + // answer.mantissa = product.high >> (upperbit + 64 - + // binary::mantissa_explicit_bits() - 3); + // ... we dropped out only zeroes. But if this happened, then we can go + // back!!! + if ((answer.mantissa << shift) == product.high) { + answer.mantissa &= ~uint64_t(1); // flip it so that we do not round up + } + } + + answer.mantissa += (answer.mantissa & 1); // round up + answer.mantissa >>= 1; + // Both fix-ups below are rare. They are marked unlikely so that clang keeps + // them as branches instead of folding them into conditional moves that + // every conversion pays for. +#ifdef __clang__ +#pragma clang diagnostic push +#if (!defined(__APPLE_CC__) && __clang_major__ >= 10) || (__clang_major__ >= 13) +#pragma clang diagnostic ignored "-Wc++20-extensions" +#endif +#endif + if fastfloat_clang_unlikely (answer.mantissa >= + (uint64_t(2) + << binary::mantissa_explicit_bits())) { + answer.mantissa = (uint64_t(1) << binary::mantissa_explicit_bits()); + answer.power2++; // undo previous addition + } + + answer.mantissa &= ~(uint64_t(1) << binary::mantissa_explicit_bits()); + if fastfloat_clang_unlikely (answer.power2 >= binary::infinite_power()) { + // infinity + answer.power2 = binary::infinite_power(); + answer.mantissa = 0; + } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + return answer; +} + +} // namespace fast_float + +#endif diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/digit_comparison.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/digit_comparison.h new file mode 100644 index 0000000000..6aac9522ee --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/digit_comparison.h @@ -0,0 +1,450 @@ +#ifndef FASTFLOAT_DIGIT_COMPARISON_H +#define FASTFLOAT_DIGIT_COMPARISON_H + +#include +#include +#include + +#include "float_common.h" +#include "bigint.h" +#include "ascii_number.h" + +namespace fast_float { + +// 1e0 to 1e19 +constexpr static uint64_t powers_of_ten_uint64[] = {1UL, + 10UL, + 100UL, + 1000UL, + 10000UL, + 100000UL, + 1000000UL, + 10000000UL, + 100000000UL, + 1000000000UL, + 10000000000UL, + 100000000000UL, + 1000000000000UL, + 10000000000000UL, + 100000000000000UL, + 1000000000000000UL, + 10000000000000000UL, + 100000000000000000UL, + 1000000000000000000UL, + 10000000000000000000UL}; + +// calculate the exponent, in scientific notation, of the number. +// this algorithm is not even close to optimized, but it has no practical +// effect on performance: in order to have a faster algorithm, we'd need +// to slow down performance for faster algorithms, and this is still fast. +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 int32_t +scientific_exponent(uint64_t mantissa, int32_t exponent) noexcept { + while (mantissa >= 10000) { + mantissa /= 10000; + exponent += 4; + } + while (mantissa >= 100) { + mantissa /= 100; + exponent += 2; + } + while (mantissa >= 10) { + mantissa /= 10; + exponent += 1; + } + return exponent; +} + +// this converts a native floating-point number to an extended-precision float. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 adjusted_mantissa +to_extended(T value) noexcept { + using equiv_uint = equiv_uint_t; + constexpr equiv_uint exponent_mask = binary_format::exponent_mask(); + constexpr equiv_uint mantissa_mask = binary_format::mantissa_mask(); + constexpr equiv_uint hidden_bit_mask = binary_format::hidden_bit_mask(); + + adjusted_mantissa am; + int32_t bias = binary_format::mantissa_explicit_bits() - + binary_format::minimum_exponent(); + equiv_uint bits; +#if FASTFLOAT_HAS_BIT_CAST + bits = std::bit_cast(value); +#else + ::memcpy(&bits, &value, sizeof(T)); +#endif + if ((bits & exponent_mask) == 0) { + // denormal + am.power2 = 1 - bias; + am.mantissa = bits & mantissa_mask; + } else { + // normal + am.power2 = int32_t((bits & exponent_mask) >> + binary_format::mantissa_explicit_bits()); + am.power2 -= bias; + am.mantissa = (bits & mantissa_mask) | hidden_bit_mask; + } + + return am; +} + +// get the extended precision value of the halfway point between b and b+u. +// we are given a native float that represents b, so we need to adjust it +// halfway between b and b+u. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 adjusted_mantissa +to_extended_halfway(T value) noexcept { + adjusted_mantissa am = to_extended(value); + am.mantissa <<= 1; + am.mantissa += 1; + am.power2 -= 1; + return am; +} + +// round an extended-precision float to the nearest machine float. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 void round(adjusted_mantissa &am, + callback cb) noexcept { + int32_t mantissa_shift = 64 - binary_format::mantissa_explicit_bits() - 1; + if (-am.power2 >= mantissa_shift) { + // have a denormal float + int32_t shift = -am.power2 + 1; + cb(am, (shift < 64 ? shift : 64)); + // check for round-up: if rounding-nearest carried us to the hidden bit. + am.power2 = (am.mantissa < + (uint64_t(1) << binary_format::mantissa_explicit_bits())) + ? 0 + : 1; + return; + } + + // have a normal float, use the default shift. + cb(am, mantissa_shift); + + // check for carry + if (am.mantissa >= + (uint64_t(2) << binary_format::mantissa_explicit_bits())) { + am.mantissa = (uint64_t(1) << binary_format::mantissa_explicit_bits()); + am.power2++; + } + + // check for infinite: we could have carried to an infinite power + am.mantissa &= ~(uint64_t(1) << binary_format::mantissa_explicit_bits()); + if (am.power2 >= binary_format::infinite_power()) { + am.power2 = binary_format::infinite_power(); + am.mantissa = 0; + } +} + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 void +round_nearest_tie_even(adjusted_mantissa &am, int32_t shift, + callback cb) noexcept { + uint64_t const mask = (shift == 64) ? UINT64_MAX : (uint64_t(1) << shift) - 1; + uint64_t const halfway = (shift == 0) ? 0 : uint64_t(1) << (shift - 1); + uint64_t truncated_bits = am.mantissa & mask; + bool is_above = truncated_bits > halfway; + bool is_halfway = truncated_bits == halfway; + + // shift digits into position + if (shift == 64) { + am.mantissa = 0; + } else { + am.mantissa >>= shift; + } + am.power2 += shift; + + bool is_odd = (am.mantissa & 1) == 1; + am.mantissa += uint64_t(cb(is_odd, is_halfway, is_above)); +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 void +round_down(adjusted_mantissa &am, int32_t shift) noexcept { + if (shift == 64) { + am.mantissa = 0; + } else { + am.mantissa >>= shift; + } + am.power2 += shift; +} + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 void +skip_zeros(UC const *&first, UC const *last) noexcept { + uint64_t val; + while (!cpp20_and_in_constexpr() && + std::distance(first, last) >= int_cmp_len()) { + ::memcpy(&val, first, sizeof(uint64_t)); + if (val != int_cmp_zeros()) { + break; + } + first += int_cmp_len(); + } + while (first != last) { + if (*first != UC('0')) { + break; + } + first++; + } +} + +// determine if any non-zero digits were truncated. +// all characters must be valid digits. +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool +is_truncated(UC const *first, UC const *last) noexcept { + // do 8-bit optimizations, can just compare to 8 literal 0s. + uint64_t val; + while (!cpp20_and_in_constexpr() && + std::distance(first, last) >= int_cmp_len()) { + ::memcpy(&val, first, sizeof(uint64_t)); + if (val != int_cmp_zeros()) { + return true; + } + first += int_cmp_len(); + } + while (first != last) { + if (*first != UC('0')) { + return true; + } + ++first; + } + return false; +} + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool +is_truncated(span s) noexcept { + return is_truncated(s.ptr, s.ptr + s.len()); +} + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 void +parse_eight_digits(UC const *&p, limb &value, size_t &counter, + size_t &count) noexcept { + value = value * 100000000 + parse_eight_digits_unrolled(p); + p += 8; + counter += 8; + count += 8; +} + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 void +parse_one_digit(UC const *&p, limb &value, size_t &counter, + size_t &count) noexcept { + value = value * 10 + limb(*p - UC('0')); + p++; + counter++; + count++; +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 void +add_native(bigint &big, limb power, limb value) noexcept { + big.mul(power); + big.add(value); +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 void +round_up_bigint(bigint &big, size_t &count) noexcept { + // need to round-up the digits, but need to avoid rounding + // ....9999 to ...10000, which could cause a false halfway point. + add_native(big, 10, 1); + count++; +} + +// parse the significant digits into a big integer +template +inline FASTFLOAT_CONSTEXPR20 void +parse_mantissa(bigint &result, parsed_number_string_t &num, + size_t max_digits, size_t &digits) noexcept { + // try to minimize the number of big integer and scalar multiplication. + // therefore, try to parse 8 digits at a time, and multiply by the largest + // scalar value (9 or 19 digits) for each step. + size_t counter = 0; + digits = 0; + limb value = 0; +#ifdef FASTFLOAT_64BIT_LIMB + size_t step = 19; +#else + size_t step = 9; +#endif + + // process all integer digits. + UC const *p = num.integer.ptr; + UC const *pend = p + num.integer.len(); + skip_zeros(p, pend); + // process all digits, in increments of step per loop + while (p != pend) { + while ((std::distance(p, pend) >= 8) && (step - counter >= 8) && + (max_digits - digits >= 8)) { + parse_eight_digits(p, value, counter, digits); + } + while (counter < step && p != pend && digits < max_digits) { + parse_one_digit(p, value, counter, digits); + } + if (digits == max_digits) { + // add the temporary value, then check if we've truncated any digits + add_native(result, limb(powers_of_ten_uint64[counter]), value); + bool truncated = is_truncated(p, pend); + if (num.fraction.ptr != nullptr) { + truncated |= is_truncated(num.fraction); + } + if (truncated) { + round_up_bigint(result, digits); + } + return; + } else { + add_native(result, limb(powers_of_ten_uint64[counter]), value); + counter = 0; + value = 0; + } + } + + // add our fraction digits, if they're available. + if (num.fraction.ptr != nullptr) { + p = num.fraction.ptr; + pend = p + num.fraction.len(); + if (digits == 0) { + skip_zeros(p, pend); + } + // process all digits, in increments of step per loop + while (p != pend) { + while ((std::distance(p, pend) >= 8) && (step - counter >= 8) && + (max_digits - digits >= 8)) { + parse_eight_digits(p, value, counter, digits); + } + while (counter < step && p != pend && digits < max_digits) { + parse_one_digit(p, value, counter, digits); + } + if (digits == max_digits) { + // add the temporary value, then check if we've truncated any digits + add_native(result, limb(powers_of_ten_uint64[counter]), value); + bool truncated = is_truncated(p, pend); + if (truncated) { + round_up_bigint(result, digits); + } + return; + } else { + add_native(result, limb(powers_of_ten_uint64[counter]), value); + counter = 0; + value = 0; + } + } + } +} + +template +inline FASTFLOAT_CONSTEXPR20 adjusted_mantissa +positive_digit_comp(bigint &bigmant, int32_t exponent) noexcept { + FASTFLOAT_ASSERT(bigmant.pow10(uint32_t(exponent))); + adjusted_mantissa answer; + bool truncated; + answer.mantissa = bigmant.hi64(truncated); + int bias = binary_format::mantissa_explicit_bits() - + binary_format::minimum_exponent(); + answer.power2 = bigmant.bit_length() - 64 + bias; + + round(answer, [truncated](adjusted_mantissa &a, int32_t shift) { + round_nearest_tie_even( + a, shift, + [truncated](bool is_odd, bool is_halfway, bool is_above) -> bool { + return is_above || (is_halfway && truncated) || + (is_odd && is_halfway); + }); + }); + + return answer; +} + +// the scaling here is quite simple: we have, for the real digits `m * 10^e`, +// and for the theoretical digits `n * 2^f`. Since `e` is always negative, +// to scale them identically, we do `n * 2^f * 5^-f`, so we now have `m * 2^e`. +// we then need to scale by `2^(f- e)`, and then the two significant digits +// are of the same magnitude. +template +inline FASTFLOAT_CONSTEXPR20 adjusted_mantissa negative_digit_comp( + bigint &bigmant, adjusted_mantissa am, int32_t exponent) noexcept { + bigint &real_digits = bigmant; + int32_t real_exp = exponent; + + // get the value of `b`, rounded down, and get a bigint representation of b+h + adjusted_mantissa am_b = am; + // gcc7 buf: use a lambda to remove the noexcept qualifier bug with + // -Wnoexcept-type. + round(am_b, + [](adjusted_mantissa &a, int32_t shift) { round_down(a, shift); }); + T b; + to_float(false, am_b, b); + adjusted_mantissa theor = to_extended_halfway(b); + bigint theor_digits(theor.mantissa); + int32_t theor_exp = theor.power2; + + // scale real digits and theor digits to be same power. + int32_t pow2_exp = theor_exp - real_exp; + uint32_t pow5_exp = uint32_t(-real_exp); + if (pow5_exp != 0) { + FASTFLOAT_ASSERT(theor_digits.pow5(pow5_exp)); + } + if (pow2_exp > 0) { + FASTFLOAT_ASSERT(theor_digits.pow2(uint32_t(pow2_exp))); + } else if (pow2_exp < 0) { + FASTFLOAT_ASSERT(real_digits.pow2(uint32_t(-pow2_exp))); + } + + // compare digits, and use it to direct rounding + int ord = real_digits.compare(theor_digits); + adjusted_mantissa answer = am; + round(answer, [ord](adjusted_mantissa &a, int32_t shift) { + round_nearest_tie_even( + a, shift, [ord](bool is_odd, bool _, bool __) -> bool { + static_cast(_); // not needed, since we've done our comparison + static_cast(__); // not needed, since we've done our comparison + if (ord > 0) { + return true; + } else if (ord < 0) { + return false; + } else { + return is_odd; + } + }); + }); + + return answer; +} + +// parse the significant digits as a big integer to unambiguously round +// the significant digits. here, we are trying to determine how to round +// an extended float representation close to `b+h`, halfway between `b` +// (the float rounded-down) and `b+u`, the next positive float. this +// algorithm is always correct, and uses one of two approaches. when +// the exponent is positive relative to the significant digits (such as +// 1234), we create a big-integer representation, get the high 64-bits, +// determine if any lower bits are truncated, and use that to direct +// rounding. in case of a negative exponent relative to the significant +// digits (such as 1.2345), we create a theoretical representation of +// `b` as a big-integer type, scaled to the same binary exponent as +// the actual digits. we then compare the big integer representations +// of both, and use that to direct rounding. +template +inline FASTFLOAT_CONSTEXPR20 adjusted_mantissa +digit_comp(parsed_number_string_t &num, adjusted_mantissa am) noexcept { + // remove the invalid exponent bias + am.power2 -= invalid_am_bias; + + int32_t sci_exp = + scientific_exponent(num.mantissa, static_cast(num.exponent)); + size_t max_digits = binary_format::max_digits(); + size_t digits = 0; + bigint bigmant; + parse_mantissa(bigmant, num, max_digits, digits); + // can't underflow, since digits is at most max_digits. + int32_t exponent = sci_exp + 1 - int32_t(digits); + if (exponent >= 0) { + return positive_digit_comp(bigmant, exponent); + } else { + return negative_digit_comp(bigmant, am, exponent); + } +} + +} // namespace fast_float + +#endif diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/fast_float.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/fast_float.h new file mode 100644 index 0000000000..1e79ac612e --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/fast_float.h @@ -0,0 +1,91 @@ + +#ifndef FASTFLOAT_FAST_FLOAT_H +#define FASTFLOAT_FAST_FLOAT_H + +#include "float_common.h" + +namespace fast_float { +/** + * This function parses the character sequence [first,last) for a number. It + * parses floating-point numbers expecting a locale-independent format + * equivalent to what is used by std::strtod in the default ("C") locale. The + * resulting floating-point value is the closest floating-point values (using + * either float or double), using the "round to even" convention for values that + * would otherwise fall right in-between two values. That is, we provide exact + * parsing according to the IEEE standard. + * + * Given a successful parse, the pointer (`ptr`) in the returned value is set to + * point right after the parsed number, and the `value` referenced is set to the + * parsed value. In case of error, the returned `ec` contains a representative + * error, otherwise the default (`std::errc()`) value is stored. + * + * The implementation does not throw and does not allocate memory (e.g., with + * `new` or `malloc`). + * + * Like the C++17 standard, the `fast_float::from_chars` functions take an + * optional last argument of the type `fast_float::chars_format`. It is a bitset + * value: we check whether `fmt & fast_float::chars_format::fixed` and `fmt & + * fast_float::chars_format::scientific` are set to determine whether we allow + * the fixed point and scientific notation respectively. The default is + * `fast_float::chars_format::general` which allows both `fixed` and + * `scientific`. + */ +template ::value)> +FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars(UC const *first, UC const *last, T &value, + chars_format fmt = chars_format::general) noexcept; + +/** + * Like from_chars, but accepts an `options` argument to govern number parsing. + * Both for floating-point types and integer types. + */ +template +FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars_advanced(UC const *first, UC const *last, T &value, + parse_options_t options) noexcept; + +/** + * This function multiplies an integer number by a power of 10 and returns + * the result as a double precision floating-point value that is correctly + * rounded. The resulting floating-point value is the closest floating-point + * value, using the "round to nearest, tie to even" convention for values that + * would otherwise fall right in-between two values. That is, we provide exact + * conversion according to the IEEE standard. + * + * On overflow infinity is returned, on underflow 0 is returned. + * + * The implementation does not throw and does not allocate memory (e.g., with + * `new` or `malloc`). + */ +FASTFLOAT_CONSTEXPR20 inline double +integer_times_pow10(uint64_t mantissa, int decimal_exponent) noexcept; +FASTFLOAT_CONSTEXPR20 inline double +integer_times_pow10(int64_t mantissa, int decimal_exponent) noexcept; + +/** + * This function is a template overload of `integer_times_pow10()` + * that returns a floating-point value of type `T` that is one of + * supported floating-point types (e.g. `double`, `float`). + */ +template +FASTFLOAT_CONSTEXPR20 + typename std::enable_if::value, T>::type + integer_times_pow10(uint64_t mantissa, int decimal_exponent) noexcept; +template +FASTFLOAT_CONSTEXPR20 + typename std::enable_if::value, T>::type + integer_times_pow10(int64_t mantissa, int decimal_exponent) noexcept; + +/** + * from_chars for integer types. + */ +template ::value)> +FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars(UC const *first, UC const *last, T &value, int base = 10) noexcept; + +} // namespace fast_float + +#include "parse_number.h" +#endif // FASTFLOAT_FAST_FLOAT_H diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/fast_table.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/fast_table.h new file mode 100644 index 0000000000..69f9b2c924 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/fast_table.h @@ -0,0 +1,708 @@ +#ifndef FASTFLOAT_FAST_TABLE_H +#define FASTFLOAT_FAST_TABLE_H + +#include + +namespace fast_float { + +/** + * When mapping numbers from decimal to binary, + * we go from w * 10^q to m * 2^p but we have + * 10^q = 5^q * 2^q, so effectively + * we are trying to match + * w * 2^q * 5^q to m * 2^p. Thus the powers of two + * are not a concern since they can be represented + * exactly using the binary notation, only the powers of five + * affect the binary significand. + */ + +/** + * The smallest non-zero float (binary64) is 2^-1074. + * We take as input numbers of the form w x 10^q where w < 2^64. + * We have that w * 10^-343 < 2^(64-344) 5^-343 < 2^-1076. + * However, we have that + * (2^64-1) * 10^-342 = (2^64-1) * 2^-342 * 5^-342 > 2^-1074. + * Thus it is possible for a number of the form w * 10^-342 where + * w is a 64-bit value to be a non-zero floating-point number. + ********* + * Any number of form w * 10^309 where w>= 1 is going to be + * infinite in binary64 so we never need to worry about powers + * of 5 greater than 308. + */ +template struct powers_template { + + constexpr static int smallest_power_of_five = + binary_format::smallest_power_of_ten(); + constexpr static int largest_power_of_five = + binary_format::largest_power_of_ten(); + constexpr static int number_of_entries = + 2 * (largest_power_of_five - smallest_power_of_five + 1); + // Powers of five from 5^-342 all the way to 5^308 rounded toward one. + constexpr static uint64_t power_of_five_128[number_of_entries] = { + 0xeef453d6923bd65a, 0x113faa2906a13b3f, + 0x9558b4661b6565f8, 0x4ac7ca59a424c507, + 0xbaaee17fa23ebf76, 0x5d79bcf00d2df649, + 0xe95a99df8ace6f53, 0xf4d82c2c107973dc, + 0x91d8a02bb6c10594, 0x79071b9b8a4be869, + 0xb64ec836a47146f9, 0x9748e2826cdee284, + 0xe3e27a444d8d98b7, 0xfd1b1b2308169b25, + 0x8e6d8c6ab0787f72, 0xfe30f0f5e50e20f7, + 0xb208ef855c969f4f, 0xbdbd2d335e51a935, + 0xde8b2b66b3bc4723, 0xad2c788035e61382, + 0x8b16fb203055ac76, 0x4c3bcb5021afcc31, + 0xaddcb9e83c6b1793, 0xdf4abe242a1bbf3d, + 0xd953e8624b85dd78, 0xd71d6dad34a2af0d, + 0x87d4713d6f33aa6b, 0x8672648c40e5ad68, + 0xa9c98d8ccb009506, 0x680efdaf511f18c2, + 0xd43bf0effdc0ba48, 0x212bd1b2566def2, + 0x84a57695fe98746d, 0x14bb630f7604b57, + 0xa5ced43b7e3e9188, 0x419ea3bd35385e2d, + 0xcf42894a5dce35ea, 0x52064cac828675b9, + 0x818995ce7aa0e1b2, 0x7343efebd1940993, + 0xa1ebfb4219491a1f, 0x1014ebe6c5f90bf8, + 0xca66fa129f9b60a6, 0xd41a26e077774ef6, + 0xfd00b897478238d0, 0x8920b098955522b4, + 0x9e20735e8cb16382, 0x55b46e5f5d5535b0, + 0xc5a890362fddbc62, 0xeb2189f734aa831d, + 0xf712b443bbd52b7b, 0xa5e9ec7501d523e4, + 0x9a6bb0aa55653b2d, 0x47b233c92125366e, + 0xc1069cd4eabe89f8, 0x999ec0bb696e840a, + 0xf148440a256e2c76, 0xc00670ea43ca250d, + 0x96cd2a865764dbca, 0x380406926a5e5728, + 0xbc807527ed3e12bc, 0xc605083704f5ecf2, + 0xeba09271e88d976b, 0xf7864a44c633682e, + 0x93445b8731587ea3, 0x7ab3ee6afbe0211d, + 0xb8157268fdae9e4c, 0x5960ea05bad82964, + 0xe61acf033d1a45df, 0x6fb92487298e33bd, + 0x8fd0c16206306bab, 0xa5d3b6d479f8e056, + 0xb3c4f1ba87bc8696, 0x8f48a4899877186c, + 0xe0b62e2929aba83c, 0x331acdabfe94de87, + 0x8c71dcd9ba0b4925, 0x9ff0c08b7f1d0b14, + 0xaf8e5410288e1b6f, 0x7ecf0ae5ee44dd9, + 0xdb71e91432b1a24a, 0xc9e82cd9f69d6150, + 0x892731ac9faf056e, 0xbe311c083a225cd2, + 0xab70fe17c79ac6ca, 0x6dbd630a48aaf406, + 0xd64d3d9db981787d, 0x92cbbccdad5b108, + 0x85f0468293f0eb4e, 0x25bbf56008c58ea5, + 0xa76c582338ed2621, 0xaf2af2b80af6f24e, + 0xd1476e2c07286faa, 0x1af5af660db4aee1, + 0x82cca4db847945ca, 0x50d98d9fc890ed4d, + 0xa37fce126597973c, 0xe50ff107bab528a0, + 0xcc5fc196fefd7d0c, 0x1e53ed49a96272c8, + 0xff77b1fcbebcdc4f, 0x25e8e89c13bb0f7a, + 0x9faacf3df73609b1, 0x77b191618c54e9ac, + 0xc795830d75038c1d, 0xd59df5b9ef6a2417, + 0xf97ae3d0d2446f25, 0x4b0573286b44ad1d, + 0x9becce62836ac577, 0x4ee367f9430aec32, + 0xc2e801fb244576d5, 0x229c41f793cda73f, + 0xf3a20279ed56d48a, 0x6b43527578c1110f, + 0x9845418c345644d6, 0x830a13896b78aaa9, + 0xbe5691ef416bd60c, 0x23cc986bc656d553, + 0xedec366b11c6cb8f, 0x2cbfbe86b7ec8aa8, + 0x94b3a202eb1c3f39, 0x7bf7d71432f3d6a9, + 0xb9e08a83a5e34f07, 0xdaf5ccd93fb0cc53, + 0xe858ad248f5c22c9, 0xd1b3400f8f9cff68, + 0x91376c36d99995be, 0x23100809b9c21fa1, + 0xb58547448ffffb2d, 0xabd40a0c2832a78a, + 0xe2e69915b3fff9f9, 0x16c90c8f323f516c, + 0x8dd01fad907ffc3b, 0xae3da7d97f6792e3, + 0xb1442798f49ffb4a, 0x99cd11cfdf41779c, + 0xdd95317f31c7fa1d, 0x40405643d711d583, + 0x8a7d3eef7f1cfc52, 0x482835ea666b2572, + 0xad1c8eab5ee43b66, 0xda3243650005eecf, + 0xd863b256369d4a40, 0x90bed43e40076a82, + 0x873e4f75e2224e68, 0x5a7744a6e804a291, + 0xa90de3535aaae202, 0x711515d0a205cb36, + 0xd3515c2831559a83, 0xd5a5b44ca873e03, + 0x8412d9991ed58091, 0xe858790afe9486c2, + 0xa5178fff668ae0b6, 0x626e974dbe39a872, + 0xce5d73ff402d98e3, 0xfb0a3d212dc8128f, + 0x80fa687f881c7f8e, 0x7ce66634bc9d0b99, + 0xa139029f6a239f72, 0x1c1fffc1ebc44e80, + 0xc987434744ac874e, 0xa327ffb266b56220, + 0xfbe9141915d7a922, 0x4bf1ff9f0062baa8, + 0x9d71ac8fada6c9b5, 0x6f773fc3603db4a9, + 0xc4ce17b399107c22, 0xcb550fb4384d21d3, + 0xf6019da07f549b2b, 0x7e2a53a146606a48, + 0x99c102844f94e0fb, 0x2eda7444cbfc426d, + 0xc0314325637a1939, 0xfa911155fefb5308, + 0xf03d93eebc589f88, 0x793555ab7eba27ca, + 0x96267c7535b763b5, 0x4bc1558b2f3458de, + 0xbbb01b9283253ca2, 0x9eb1aaedfb016f16, + 0xea9c227723ee8bcb, 0x465e15a979c1cadc, + 0x92a1958a7675175f, 0xbfacd89ec191ec9, + 0xb749faed14125d36, 0xcef980ec671f667b, + 0xe51c79a85916f484, 0x82b7e12780e7401a, + 0x8f31cc0937ae58d2, 0xd1b2ecb8b0908810, + 0xb2fe3f0b8599ef07, 0x861fa7e6dcb4aa15, + 0xdfbdcece67006ac9, 0x67a791e093e1d49a, + 0x8bd6a141006042bd, 0xe0c8bb2c5c6d24e0, + 0xaecc49914078536d, 0x58fae9f773886e18, + 0xda7f5bf590966848, 0xaf39a475506a899e, + 0x888f99797a5e012d, 0x6d8406c952429603, + 0xaab37fd7d8f58178, 0xc8e5087ba6d33b83, + 0xd5605fcdcf32e1d6, 0xfb1e4a9a90880a64, + 0x855c3be0a17fcd26, 0x5cf2eea09a55067f, + 0xa6b34ad8c9dfc06f, 0xf42faa48c0ea481e, + 0xd0601d8efc57b08b, 0xf13b94daf124da26, + 0x823c12795db6ce57, 0x76c53d08d6b70858, + 0xa2cb1717b52481ed, 0x54768c4b0c64ca6e, + 0xcb7ddcdda26da268, 0xa9942f5dcf7dfd09, + 0xfe5d54150b090b02, 0xd3f93b35435d7c4c, + 0x9efa548d26e5a6e1, 0xc47bc5014a1a6daf, + 0xc6b8e9b0709f109a, 0x359ab6419ca1091b, + 0xf867241c8cc6d4c0, 0xc30163d203c94b62, + 0x9b407691d7fc44f8, 0x79e0de63425dcf1d, + 0xc21094364dfb5636, 0x985915fc12f542e4, + 0xf294b943e17a2bc4, 0x3e6f5b7b17b2939d, + 0x979cf3ca6cec5b5a, 0xa705992ceecf9c42, + 0xbd8430bd08277231, 0x50c6ff782a838353, + 0xece53cec4a314ebd, 0xa4f8bf5635246428, + 0x940f4613ae5ed136, 0x871b7795e136be99, + 0xb913179899f68584, 0x28e2557b59846e3f, + 0xe757dd7ec07426e5, 0x331aeada2fe589cf, + 0x9096ea6f3848984f, 0x3ff0d2c85def7621, + 0xb4bca50b065abe63, 0xfed077a756b53a9, + 0xe1ebce4dc7f16dfb, 0xd3e8495912c62894, + 0x8d3360f09cf6e4bd, 0x64712dd7abbbd95c, + 0xb080392cc4349dec, 0xbd8d794d96aacfb3, + 0xdca04777f541c567, 0xecf0d7a0fc5583a0, + 0x89e42caaf9491b60, 0xf41686c49db57244, + 0xac5d37d5b79b6239, 0x311c2875c522ced5, + 0xd77485cb25823ac7, 0x7d633293366b828b, + 0x86a8d39ef77164bc, 0xae5dff9c02033197, + 0xa8530886b54dbdeb, 0xd9f57f830283fdfc, + 0xd267caa862a12d66, 0xd072df63c324fd7b, + 0x8380dea93da4bc60, 0x4247cb9e59f71e6d, + 0xa46116538d0deb78, 0x52d9be85f074e608, + 0xcd795be870516656, 0x67902e276c921f8b, + 0x806bd9714632dff6, 0xba1cd8a3db53b6, + 0xa086cfcd97bf97f3, 0x80e8a40eccd228a4, + 0xc8a883c0fdaf7df0, 0x6122cd128006b2cd, + 0xfad2a4b13d1b5d6c, 0x796b805720085f81, + 0x9cc3a6eec6311a63, 0xcbe3303674053bb0, + 0xc3f490aa77bd60fc, 0xbedbfc4411068a9c, + 0xf4f1b4d515acb93b, 0xee92fb5515482d44, + 0x991711052d8bf3c5, 0x751bdd152d4d1c4a, + 0xbf5cd54678eef0b6, 0xd262d45a78a0635d, + 0xef340a98172aace4, 0x86fb897116c87c34, + 0x9580869f0e7aac0e, 0xd45d35e6ae3d4da0, + 0xbae0a846d2195712, 0x8974836059cca109, + 0xe998d258869facd7, 0x2bd1a438703fc94b, + 0x91ff83775423cc06, 0x7b6306a34627ddcf, + 0xb67f6455292cbf08, 0x1a3bc84c17b1d542, + 0xe41f3d6a7377eeca, 0x20caba5f1d9e4a93, + 0x8e938662882af53e, 0x547eb47b7282ee9c, + 0xb23867fb2a35b28d, 0xe99e619a4f23aa43, + 0xdec681f9f4c31f31, 0x6405fa00e2ec94d4, + 0x8b3c113c38f9f37e, 0xde83bc408dd3dd04, + 0xae0b158b4738705e, 0x9624ab50b148d445, + 0xd98ddaee19068c76, 0x3badd624dd9b0957, + 0x87f8a8d4cfa417c9, 0xe54ca5d70a80e5d6, + 0xa9f6d30a038d1dbc, 0x5e9fcf4ccd211f4c, + 0xd47487cc8470652b, 0x7647c3200069671f, + 0x84c8d4dfd2c63f3b, 0x29ecd9f40041e073, + 0xa5fb0a17c777cf09, 0xf468107100525890, + 0xcf79cc9db955c2cc, 0x7182148d4066eeb4, + 0x81ac1fe293d599bf, 0xc6f14cd848405530, + 0xa21727db38cb002f, 0xb8ada00e5a506a7c, + 0xca9cf1d206fdc03b, 0xa6d90811f0e4851c, + 0xfd442e4688bd304a, 0x908f4a166d1da663, + 0x9e4a9cec15763e2e, 0x9a598e4e043287fe, + 0xc5dd44271ad3cdba, 0x40eff1e1853f29fd, + 0xf7549530e188c128, 0xd12bee59e68ef47c, + 0x9a94dd3e8cf578b9, 0x82bb74f8301958ce, + 0xc13a148e3032d6e7, 0xe36a52363c1faf01, + 0xf18899b1bc3f8ca1, 0xdc44e6c3cb279ac1, + 0x96f5600f15a7b7e5, 0x29ab103a5ef8c0b9, + 0xbcb2b812db11a5de, 0x7415d448f6b6f0e7, + 0xebdf661791d60f56, 0x111b495b3464ad21, + 0x936b9fcebb25c995, 0xcab10dd900beec34, + 0xb84687c269ef3bfb, 0x3d5d514f40eea742, + 0xe65829b3046b0afa, 0xcb4a5a3112a5112, + 0x8ff71a0fe2c2e6dc, 0x47f0e785eaba72ab, + 0xb3f4e093db73a093, 0x59ed216765690f56, + 0xe0f218b8d25088b8, 0x306869c13ec3532c, + 0x8c974f7383725573, 0x1e414218c73a13fb, + 0xafbd2350644eeacf, 0xe5d1929ef90898fa, + 0xdbac6c247d62a583, 0xdf45f746b74abf39, + 0x894bc396ce5da772, 0x6b8bba8c328eb783, + 0xab9eb47c81f5114f, 0x66ea92f3f326564, + 0xd686619ba27255a2, 0xc80a537b0efefebd, + 0x8613fd0145877585, 0xbd06742ce95f5f36, + 0xa798fc4196e952e7, 0x2c48113823b73704, + 0xd17f3b51fca3a7a0, 0xf75a15862ca504c5, + 0x82ef85133de648c4, 0x9a984d73dbe722fb, + 0xa3ab66580d5fdaf5, 0xc13e60d0d2e0ebba, + 0xcc963fee10b7d1b3, 0x318df905079926a8, + 0xffbbcfe994e5c61f, 0xfdf17746497f7052, + 0x9fd561f1fd0f9bd3, 0xfeb6ea8bedefa633, + 0xc7caba6e7c5382c8, 0xfe64a52ee96b8fc0, + 0xf9bd690a1b68637b, 0x3dfdce7aa3c673b0, + 0x9c1661a651213e2d, 0x6bea10ca65c084e, + 0xc31bfa0fe5698db8, 0x486e494fcff30a62, + 0xf3e2f893dec3f126, 0x5a89dba3c3efccfa, + 0x986ddb5c6b3a76b7, 0xf89629465a75e01c, + 0xbe89523386091465, 0xf6bbb397f1135823, + 0xee2ba6c0678b597f, 0x746aa07ded582e2c, + 0x94db483840b717ef, 0xa8c2a44eb4571cdc, + 0xba121a4650e4ddeb, 0x92f34d62616ce413, + 0xe896a0d7e51e1566, 0x77b020baf9c81d17, + 0x915e2486ef32cd60, 0xace1474dc1d122e, + 0xb5b5ada8aaff80b8, 0xd819992132456ba, + 0xe3231912d5bf60e6, 0x10e1fff697ed6c69, + 0x8df5efabc5979c8f, 0xca8d3ffa1ef463c1, + 0xb1736b96b6fd83b3, 0xbd308ff8a6b17cb2, + 0xddd0467c64bce4a0, 0xac7cb3f6d05ddbde, + 0x8aa22c0dbef60ee4, 0x6bcdf07a423aa96b, + 0xad4ab7112eb3929d, 0x86c16c98d2c953c6, + 0xd89d64d57a607744, 0xe871c7bf077ba8b7, + 0x87625f056c7c4a8b, 0x11471cd764ad4972, + 0xa93af6c6c79b5d2d, 0xd598e40d3dd89bcf, + 0xd389b47879823479, 0x4aff1d108d4ec2c3, + 0x843610cb4bf160cb, 0xcedf722a585139ba, + 0xa54394fe1eedb8fe, 0xc2974eb4ee658828, + 0xce947a3da6a9273e, 0x733d226229feea32, + 0x811ccc668829b887, 0x806357d5a3f525f, + 0xa163ff802a3426a8, 0xca07c2dcb0cf26f7, + 0xc9bcff6034c13052, 0xfc89b393dd02f0b5, + 0xfc2c3f3841f17c67, 0xbbac2078d443ace2, + 0x9d9ba7832936edc0, 0xd54b944b84aa4c0d, + 0xc5029163f384a931, 0xa9e795e65d4df11, + 0xf64335bcf065d37d, 0x4d4617b5ff4a16d5, + 0x99ea0196163fa42e, 0x504bced1bf8e4e45, + 0xc06481fb9bcf8d39, 0xe45ec2862f71e1d6, + 0xf07da27a82c37088, 0x5d767327bb4e5a4c, + 0x964e858c91ba2655, 0x3a6a07f8d510f86f, + 0xbbe226efb628afea, 0x890489f70a55368b, + 0xeadab0aba3b2dbe5, 0x2b45ac74ccea842e, + 0x92c8ae6b464fc96f, 0x3b0b8bc90012929d, + 0xb77ada0617e3bbcb, 0x9ce6ebb40173744, + 0xe55990879ddcaabd, 0xcc420a6a101d0515, + 0x8f57fa54c2a9eab6, 0x9fa946824a12232d, + 0xb32df8e9f3546564, 0x47939822dc96abf9, + 0xdff9772470297ebd, 0x59787e2b93bc56f7, + 0x8bfbea76c619ef36, 0x57eb4edb3c55b65a, + 0xaefae51477a06b03, 0xede622920b6b23f1, + 0xdab99e59958885c4, 0xe95fab368e45eced, + 0x88b402f7fd75539b, 0x11dbcb0218ebb414, + 0xaae103b5fcd2a881, 0xd652bdc29f26a119, + 0xd59944a37c0752a2, 0x4be76d3346f0495f, + 0x857fcae62d8493a5, 0x6f70a4400c562ddb, + 0xa6dfbd9fb8e5b88e, 0xcb4ccd500f6bb952, + 0xd097ad07a71f26b2, 0x7e2000a41346a7a7, + 0x825ecc24c873782f, 0x8ed400668c0c28c8, + 0xa2f67f2dfa90563b, 0x728900802f0f32fa, + 0xcbb41ef979346bca, 0x4f2b40a03ad2ffb9, + 0xfea126b7d78186bc, 0xe2f610c84987bfa8, + 0x9f24b832e6b0f436, 0xdd9ca7d2df4d7c9, + 0xc6ede63fa05d3143, 0x91503d1c79720dbb, + 0xf8a95fcf88747d94, 0x75a44c6397ce912a, + 0x9b69dbe1b548ce7c, 0xc986afbe3ee11aba, + 0xc24452da229b021b, 0xfbe85badce996168, + 0xf2d56790ab41c2a2, 0xfae27299423fb9c3, + 0x97c560ba6b0919a5, 0xdccd879fc967d41a, + 0xbdb6b8e905cb600f, 0x5400e987bbc1c920, + 0xed246723473e3813, 0x290123e9aab23b68, + 0x9436c0760c86e30b, 0xf9a0b6720aaf6521, + 0xb94470938fa89bce, 0xf808e40e8d5b3e69, + 0xe7958cb87392c2c2, 0xb60b1d1230b20e04, + 0x90bd77f3483bb9b9, 0xb1c6f22b5e6f48c2, + 0xb4ecd5f01a4aa828, 0x1e38aeb6360b1af3, + 0xe2280b6c20dd5232, 0x25c6da63c38de1b0, + 0x8d590723948a535f, 0x579c487e5a38ad0e, + 0xb0af48ec79ace837, 0x2d835a9df0c6d851, + 0xdcdb1b2798182244, 0xf8e431456cf88e65, + 0x8a08f0f8bf0f156b, 0x1b8e9ecb641b58ff, + 0xac8b2d36eed2dac5, 0xe272467e3d222f3f, + 0xd7adf884aa879177, 0x5b0ed81dcc6abb0f, + 0x86ccbb52ea94baea, 0x98e947129fc2b4e9, + 0xa87fea27a539e9a5, 0x3f2398d747b36224, + 0xd29fe4b18e88640e, 0x8eec7f0d19a03aad, + 0x83a3eeeef9153e89, 0x1953cf68300424ac, + 0xa48ceaaab75a8e2b, 0x5fa8c3423c052dd7, + 0xcdb02555653131b6, 0x3792f412cb06794d, + 0x808e17555f3ebf11, 0xe2bbd88bbee40bd0, + 0xa0b19d2ab70e6ed6, 0x5b6aceaeae9d0ec4, + 0xc8de047564d20a8b, 0xf245825a5a445275, + 0xfb158592be068d2e, 0xeed6e2f0f0d56712, + 0x9ced737bb6c4183d, 0x55464dd69685606b, + 0xc428d05aa4751e4c, 0xaa97e14c3c26b886, + 0xf53304714d9265df, 0xd53dd99f4b3066a8, + 0x993fe2c6d07b7fab, 0xe546a8038efe4029, + 0xbf8fdb78849a5f96, 0xde98520472bdd033, + 0xef73d256a5c0f77c, 0x963e66858f6d4440, + 0x95a8637627989aad, 0xdde7001379a44aa8, + 0xbb127c53b17ec159, 0x5560c018580d5d52, + 0xe9d71b689dde71af, 0xaab8f01e6e10b4a6, + 0x9226712162ab070d, 0xcab3961304ca70e8, + 0xb6b00d69bb55c8d1, 0x3d607b97c5fd0d22, + 0xe45c10c42a2b3b05, 0x8cb89a7db77c506a, + 0x8eb98a7a9a5b04e3, 0x77f3608e92adb242, + 0xb267ed1940f1c61c, 0x55f038b237591ed3, + 0xdf01e85f912e37a3, 0x6b6c46dec52f6688, + 0x8b61313bbabce2c6, 0x2323ac4b3b3da015, + 0xae397d8aa96c1b77, 0xabec975e0a0d081a, + 0xd9c7dced53c72255, 0x96e7bd358c904a21, + 0x881cea14545c7575, 0x7e50d64177da2e54, + 0xaa242499697392d2, 0xdde50bd1d5d0b9e9, + 0xd4ad2dbfc3d07787, 0x955e4ec64b44e864, + 0x84ec3c97da624ab4, 0xbd5af13bef0b113e, + 0xa6274bbdd0fadd61, 0xecb1ad8aeacdd58e, + 0xcfb11ead453994ba, 0x67de18eda5814af2, + 0x81ceb32c4b43fcf4, 0x80eacf948770ced7, + 0xa2425ff75e14fc31, 0xa1258379a94d028d, + 0xcad2f7f5359a3b3e, 0x96ee45813a04330, + 0xfd87b5f28300ca0d, 0x8bca9d6e188853fc, + 0x9e74d1b791e07e48, 0x775ea264cf55347e, + 0xc612062576589dda, 0x95364afe032a819e, + 0xf79687aed3eec551, 0x3a83ddbd83f52205, + 0x9abe14cd44753b52, 0xc4926a9672793543, + 0xc16d9a0095928a27, 0x75b7053c0f178294, + 0xf1c90080baf72cb1, 0x5324c68b12dd6339, + 0x971da05074da7bee, 0xd3f6fc16ebca5e04, + 0xbce5086492111aea, 0x88f4bb1ca6bcf585, + 0xec1e4a7db69561a5, 0x2b31e9e3d06c32e6, + 0x9392ee8e921d5d07, 0x3aff322e62439fd0, + 0xb877aa3236a4b449, 0x9befeb9fad487c3, + 0xe69594bec44de15b, 0x4c2ebe687989a9b4, + 0x901d7cf73ab0acd9, 0xf9d37014bf60a11, + 0xb424dc35095cd80f, 0x538484c19ef38c95, + 0xe12e13424bb40e13, 0x2865a5f206b06fba, + 0x8cbccc096f5088cb, 0xf93f87b7442e45d4, + 0xafebff0bcb24aafe, 0xf78f69a51539d749, + 0xdbe6fecebdedd5be, 0xb573440e5a884d1c, + 0x89705f4136b4a597, 0x31680a88f8953031, + 0xabcc77118461cefc, 0xfdc20d2b36ba7c3e, + 0xd6bf94d5e57a42bc, 0x3d32907604691b4d, + 0x8637bd05af6c69b5, 0xa63f9a49c2c1b110, + 0xa7c5ac471b478423, 0xfcf80dc33721d54, + 0xd1b71758e219652b, 0xd3c36113404ea4a9, + 0x83126e978d4fdf3b, 0x645a1cac083126ea, + 0xa3d70a3d70a3d70a, 0x3d70a3d70a3d70a4, + 0xcccccccccccccccc, 0xcccccccccccccccd, + 0x8000000000000000, 0x0, + 0xa000000000000000, 0x0, + 0xc800000000000000, 0x0, + 0xfa00000000000000, 0x0, + 0x9c40000000000000, 0x0, + 0xc350000000000000, 0x0, + 0xf424000000000000, 0x0, + 0x9896800000000000, 0x0, + 0xbebc200000000000, 0x0, + 0xee6b280000000000, 0x0, + 0x9502f90000000000, 0x0, + 0xba43b74000000000, 0x0, + 0xe8d4a51000000000, 0x0, + 0x9184e72a00000000, 0x0, + 0xb5e620f480000000, 0x0, + 0xe35fa931a0000000, 0x0, + 0x8e1bc9bf04000000, 0x0, + 0xb1a2bc2ec5000000, 0x0, + 0xde0b6b3a76400000, 0x0, + 0x8ac7230489e80000, 0x0, + 0xad78ebc5ac620000, 0x0, + 0xd8d726b7177a8000, 0x0, + 0x878678326eac9000, 0x0, + 0xa968163f0a57b400, 0x0, + 0xd3c21bcecceda100, 0x0, + 0x84595161401484a0, 0x0, + 0xa56fa5b99019a5c8, 0x0, + 0xcecb8f27f4200f3a, 0x0, + 0x813f3978f8940984, 0x4000000000000000, + 0xa18f07d736b90be5, 0x5000000000000000, + 0xc9f2c9cd04674ede, 0xa400000000000000, + 0xfc6f7c4045812296, 0x4d00000000000000, + 0x9dc5ada82b70b59d, 0xf020000000000000, + 0xc5371912364ce305, 0x6c28000000000000, + 0xf684df56c3e01bc6, 0xc732000000000000, + 0x9a130b963a6c115c, 0x3c7f400000000000, + 0xc097ce7bc90715b3, 0x4b9f100000000000, + 0xf0bdc21abb48db20, 0x1e86d40000000000, + 0x96769950b50d88f4, 0x1314448000000000, + 0xbc143fa4e250eb31, 0x17d955a000000000, + 0xeb194f8e1ae525fd, 0x5dcfab0800000000, + 0x92efd1b8d0cf37be, 0x5aa1cae500000000, + 0xb7abc627050305ad, 0xf14a3d9e40000000, + 0xe596b7b0c643c719, 0x6d9ccd05d0000000, + 0x8f7e32ce7bea5c6f, 0xe4820023a2000000, + 0xb35dbf821ae4f38b, 0xdda2802c8a800000, + 0xe0352f62a19e306e, 0xd50b2037ad200000, + 0x8c213d9da502de45, 0x4526f422cc340000, + 0xaf298d050e4395d6, 0x9670b12b7f410000, + 0xdaf3f04651d47b4c, 0x3c0cdd765f114000, + 0x88d8762bf324cd0f, 0xa5880a69fb6ac800, + 0xab0e93b6efee0053, 0x8eea0d047a457a00, + 0xd5d238a4abe98068, 0x72a4904598d6d880, + 0x85a36366eb71f041, 0x47a6da2b7f864750, + 0xa70c3c40a64e6c51, 0x999090b65f67d924, + 0xd0cf4b50cfe20765, 0xfff4b4e3f741cf6d, + 0x82818f1281ed449f, 0xbff8f10e7a8921a4, + 0xa321f2d7226895c7, 0xaff72d52192b6a0d, + 0xcbea6f8ceb02bb39, 0x9bf4f8a69f764490, + 0xfee50b7025c36a08, 0x2f236d04753d5b4, + 0x9f4f2726179a2245, 0x1d762422c946590, + 0xc722f0ef9d80aad6, 0x424d3ad2b7b97ef5, + 0xf8ebad2b84e0d58b, 0xd2e0898765a7deb2, + 0x9b934c3b330c8577, 0x63cc55f49f88eb2f, + 0xc2781f49ffcfa6d5, 0x3cbf6b71c76b25fb, + 0xf316271c7fc3908a, 0x8bef464e3945ef7a, + 0x97edd871cfda3a56, 0x97758bf0e3cbb5ac, + 0xbde94e8e43d0c8ec, 0x3d52eeed1cbea317, + 0xed63a231d4c4fb27, 0x4ca7aaa863ee4bdd, + 0x945e455f24fb1cf8, 0x8fe8caa93e74ef6a, + 0xb975d6b6ee39e436, 0xb3e2fd538e122b44, + 0xe7d34c64a9c85d44, 0x60dbbca87196b616, + 0x90e40fbeea1d3a4a, 0xbc8955e946fe31cd, + 0xb51d13aea4a488dd, 0x6babab6398bdbe41, + 0xe264589a4dcdab14, 0xc696963c7eed2dd1, + 0x8d7eb76070a08aec, 0xfc1e1de5cf543ca2, + 0xb0de65388cc8ada8, 0x3b25a55f43294bcb, + 0xdd15fe86affad912, 0x49ef0eb713f39ebe, + 0x8a2dbf142dfcc7ab, 0x6e3569326c784337, + 0xacb92ed9397bf996, 0x49c2c37f07965404, + 0xd7e77a8f87daf7fb, 0xdc33745ec97be906, + 0x86f0ac99b4e8dafd, 0x69a028bb3ded71a3, + 0xa8acd7c0222311bc, 0xc40832ea0d68ce0c, + 0xd2d80db02aabd62b, 0xf50a3fa490c30190, + 0x83c7088e1aab65db, 0x792667c6da79e0fa, + 0xa4b8cab1a1563f52, 0x577001b891185938, + 0xcde6fd5e09abcf26, 0xed4c0226b55e6f86, + 0x80b05e5ac60b6178, 0x544f8158315b05b4, + 0xa0dc75f1778e39d6, 0x696361ae3db1c721, + 0xc913936dd571c84c, 0x3bc3a19cd1e38e9, + 0xfb5878494ace3a5f, 0x4ab48a04065c723, + 0x9d174b2dcec0e47b, 0x62eb0d64283f9c76, + 0xc45d1df942711d9a, 0x3ba5d0bd324f8394, + 0xf5746577930d6500, 0xca8f44ec7ee36479, + 0x9968bf6abbe85f20, 0x7e998b13cf4e1ecb, + 0xbfc2ef456ae276e8, 0x9e3fedd8c321a67e, + 0xefb3ab16c59b14a2, 0xc5cfe94ef3ea101e, + 0x95d04aee3b80ece5, 0xbba1f1d158724a12, + 0xbb445da9ca61281f, 0x2a8a6e45ae8edc97, + 0xea1575143cf97226, 0xf52d09d71a3293bd, + 0x924d692ca61be758, 0x593c2626705f9c56, + 0xb6e0c377cfa2e12e, 0x6f8b2fb00c77836c, + 0xe498f455c38b997a, 0xb6dfb9c0f956447, + 0x8edf98b59a373fec, 0x4724bd4189bd5eac, + 0xb2977ee300c50fe7, 0x58edec91ec2cb657, + 0xdf3d5e9bc0f653e1, 0x2f2967b66737e3ed, + 0x8b865b215899f46c, 0xbd79e0d20082ee74, + 0xae67f1e9aec07187, 0xecd8590680a3aa11, + 0xda01ee641a708de9, 0xe80e6f4820cc9495, + 0x884134fe908658b2, 0x3109058d147fdcdd, + 0xaa51823e34a7eede, 0xbd4b46f0599fd415, + 0xd4e5e2cdc1d1ea96, 0x6c9e18ac7007c91a, + 0x850fadc09923329e, 0x3e2cf6bc604ddb0, + 0xa6539930bf6bff45, 0x84db8346b786151c, + 0xcfe87f7cef46ff16, 0xe612641865679a63, + 0x81f14fae158c5f6e, 0x4fcb7e8f3f60c07e, + 0xa26da3999aef7749, 0xe3be5e330f38f09d, + 0xcb090c8001ab551c, 0x5cadf5bfd3072cc5, + 0xfdcb4fa002162a63, 0x73d9732fc7c8f7f6, + 0x9e9f11c4014dda7e, 0x2867e7fddcdd9afa, + 0xc646d63501a1511d, 0xb281e1fd541501b8, + 0xf7d88bc24209a565, 0x1f225a7ca91a4226, + 0x9ae757596946075f, 0x3375788de9b06958, + 0xc1a12d2fc3978937, 0x52d6b1641c83ae, + 0xf209787bb47d6b84, 0xc0678c5dbd23a49a, + 0x9745eb4d50ce6332, 0xf840b7ba963646e0, + 0xbd176620a501fbff, 0xb650e5a93bc3d898, + 0xec5d3fa8ce427aff, 0xa3e51f138ab4cebe, + 0x93ba47c980e98cdf, 0xc66f336c36b10137, + 0xb8a8d9bbe123f017, 0xb80b0047445d4184, + 0xe6d3102ad96cec1d, 0xa60dc059157491e5, + 0x9043ea1ac7e41392, 0x87c89837ad68db2f, + 0xb454e4a179dd1877, 0x29babe4598c311fb, + 0xe16a1dc9d8545e94, 0xf4296dd6fef3d67a, + 0x8ce2529e2734bb1d, 0x1899e4a65f58660c, + 0xb01ae745b101e9e4, 0x5ec05dcff72e7f8f, + 0xdc21a1171d42645d, 0x76707543f4fa1f73, + 0x899504ae72497eba, 0x6a06494a791c53a8, + 0xabfa45da0edbde69, 0x487db9d17636892, + 0xd6f8d7509292d603, 0x45a9d2845d3c42b6, + 0x865b86925b9bc5c2, 0xb8a2392ba45a9b2, + 0xa7f26836f282b732, 0x8e6cac7768d7141e, + 0xd1ef0244af2364ff, 0x3207d795430cd926, + 0x8335616aed761f1f, 0x7f44e6bd49e807b8, + 0xa402b9c5a8d3a6e7, 0x5f16206c9c6209a6, + 0xcd036837130890a1, 0x36dba887c37a8c0f, + 0x802221226be55a64, 0xc2494954da2c9789, + 0xa02aa96b06deb0fd, 0xf2db9baa10b7bd6c, + 0xc83553c5c8965d3d, 0x6f92829494e5acc7, + 0xfa42a8b73abbf48c, 0xcb772339ba1f17f9, + 0x9c69a97284b578d7, 0xff2a760414536efb, + 0xc38413cf25e2d70d, 0xfef5138519684aba, + 0xf46518c2ef5b8cd1, 0x7eb258665fc25d69, + 0x98bf2f79d5993802, 0xef2f773ffbd97a61, + 0xbeeefb584aff8603, 0xaafb550ffacfd8fa, + 0xeeaaba2e5dbf6784, 0x95ba2a53f983cf38, + 0x952ab45cfa97a0b2, 0xdd945a747bf26183, + 0xba756174393d88df, 0x94f971119aeef9e4, + 0xe912b9d1478ceb17, 0x7a37cd5601aab85d, + 0x91abb422ccb812ee, 0xac62e055c10ab33a, + 0xb616a12b7fe617aa, 0x577b986b314d6009, + 0xe39c49765fdf9d94, 0xed5a7e85fda0b80b, + 0x8e41ade9fbebc27d, 0x14588f13be847307, + 0xb1d219647ae6b31c, 0x596eb2d8ae258fc8, + 0xde469fbd99a05fe3, 0x6fca5f8ed9aef3bb, + 0x8aec23d680043bee, 0x25de7bb9480d5854, + 0xada72ccc20054ae9, 0xaf561aa79a10ae6a, + 0xd910f7ff28069da4, 0x1b2ba1518094da04, + 0x87aa9aff79042286, 0x90fb44d2f05d0842, + 0xa99541bf57452b28, 0x353a1607ac744a53, + 0xd3fa922f2d1675f2, 0x42889b8997915ce8, + 0x847c9b5d7c2e09b7, 0x69956135febada11, + 0xa59bc234db398c25, 0x43fab9837e699095, + 0xcf02b2c21207ef2e, 0x94f967e45e03f4bb, + 0x8161afb94b44f57d, 0x1d1be0eebac278f5, + 0xa1ba1ba79e1632dc, 0x6462d92a69731732, + 0xca28a291859bbf93, 0x7d7b8f7503cfdcfe, + 0xfcb2cb35e702af78, 0x5cda735244c3d43e, + 0x9defbf01b061adab, 0x3a0888136afa64a7, + 0xc56baec21c7a1916, 0x88aaa1845b8fdd0, + 0xf6c69a72a3989f5b, 0x8aad549e57273d45, + 0x9a3c2087a63f6399, 0x36ac54e2f678864b, + 0xc0cb28a98fcf3c7f, 0x84576a1bb416a7dd, + 0xf0fdf2d3f3c30b9f, 0x656d44a2a11c51d5, + 0x969eb7c47859e743, 0x9f644ae5a4b1b325, + 0xbc4665b596706114, 0x873d5d9f0dde1fee, + 0xeb57ff22fc0c7959, 0xa90cb506d155a7ea, + 0x9316ff75dd87cbd8, 0x9a7f12442d588f2, + 0xb7dcbf5354e9bece, 0xc11ed6d538aeb2f, + 0xe5d3ef282a242e81, 0x8f1668c8a86da5fa, + 0x8fa475791a569d10, 0xf96e017d694487bc, + 0xb38d92d760ec4455, 0x37c981dcc395a9ac, + 0xe070f78d3927556a, 0x85bbe253f47b1417, + 0x8c469ab843b89562, 0x93956d7478ccec8e, + 0xaf58416654a6babb, 0x387ac8d1970027b2, + 0xdb2e51bfe9d0696a, 0x6997b05fcc0319e, + 0x88fcf317f22241e2, 0x441fece3bdf81f03, + 0xab3c2fddeeaad25a, 0xd527e81cad7626c3, + 0xd60b3bd56a5586f1, 0x8a71e223d8d3b074, + 0x85c7056562757456, 0xf6872d5667844e49, + 0xa738c6bebb12d16c, 0xb428f8ac016561db, + 0xd106f86e69d785c7, 0xe13336d701beba52, + 0x82a45b450226b39c, 0xecc0024661173473, + 0xa34d721642b06084, 0x27f002d7f95d0190, + 0xcc20ce9bd35c78a5, 0x31ec038df7b441f4, + 0xff290242c83396ce, 0x7e67047175a15271, + 0x9f79a169bd203e41, 0xf0062c6e984d386, + 0xc75809c42c684dd1, 0x52c07b78a3e60868, + 0xf92e0c3537826145, 0xa7709a56ccdf8a82, + 0x9bbcc7a142b17ccb, 0x88a66076400bb691, + 0xc2abf989935ddbfe, 0x6acff893d00ea435, + 0xf356f7ebf83552fe, 0x583f6b8c4124d43, + 0x98165af37b2153de, 0xc3727a337a8b704a, + 0xbe1bf1b059e9a8d6, 0x744f18c0592e4c5c, + 0xeda2ee1c7064130c, 0x1162def06f79df73, + 0x9485d4d1c63e8be7, 0x8addcb5645ac2ba8, + 0xb9a74a0637ce2ee1, 0x6d953e2bd7173692, + 0xe8111c87c5c1ba99, 0xc8fa8db6ccdd0437, + 0x910ab1d4db9914a0, 0x1d9c9892400a22a2, + 0xb54d5e4a127f59c8, 0x2503beb6d00cab4b, + 0xe2a0b5dc971f303a, 0x2e44ae64840fd61d, + 0x8da471a9de737e24, 0x5ceaecfed289e5d2, + 0xb10d8e1456105dad, 0x7425a83e872c5f47, + 0xdd50f1996b947518, 0xd12f124e28f77719, + 0x8a5296ffe33cc92f, 0x82bd6b70d99aaa6f, + 0xace73cbfdc0bfb7b, 0x636cc64d1001550b, + 0xd8210befd30efa5a, 0x3c47f7e05401aa4e, + 0x8714a775e3e95c78, 0x65acfaec34810a71, + 0xa8d9d1535ce3b396, 0x7f1839a741a14d0d, + 0xd31045a8341ca07c, 0x1ede48111209a050, + 0x83ea2b892091e44d, 0x934aed0aab460432, + 0xa4e4b66b68b65d60, 0xf81da84d5617853f, + 0xce1de40642e3f4b9, 0x36251260ab9d668e, + 0x80d2ae83e9ce78f3, 0xc1d72b7c6b426019, + 0xa1075a24e4421730, 0xb24cf65b8612f81f, + 0xc94930ae1d529cfc, 0xdee033f26797b627, + 0xfb9b7cd9a4a7443c, 0x169840ef017da3b1, + 0x9d412e0806e88aa5, 0x8e1f289560ee864e, + 0xc491798a08a2ad4e, 0xf1a6f2bab92a27e2, + 0xf5b5d7ec8acb58a2, 0xae10af696774b1db, + 0x9991a6f3d6bf1765, 0xacca6da1e0a8ef29, + 0xbff610b0cc6edd3f, 0x17fd090a58d32af3, + 0xeff394dcff8a948e, 0xddfc4b4cef07f5b0, + 0x95f83d0a1fb69cd9, 0x4abdaf101564f98e, + 0xbb764c4ca7a4440f, 0x9d6d1ad41abe37f1, + 0xea53df5fd18d5513, 0x84c86189216dc5ed, + 0x92746b9be2f8552c, 0x32fd3cf5b4e49bb4, + 0xb7118682dbb66a77, 0x3fbc8c33221dc2a1, + 0xe4d5e82392a40515, 0xfabaf3feaa5334a, + 0x8f05b1163ba6832d, 0x29cb4d87f2a7400e, + 0xb2c71d5bca9023f8, 0x743e20e9ef511012, + 0xdf78e4b2bd342cf6, 0x914da9246b255416, + 0x8bab8eefb6409c1a, 0x1ad089b6c2f7548e, + 0xae9672aba3d0c320, 0xa184ac2473b529b1, + 0xda3c0f568cc4f3e8, 0xc9e5d72d90a2741e, + 0x8865899617fb1871, 0x7e2fa67c7a658892, + 0xaa7eebfb9df9de8d, 0xddbb901b98feeab7, + 0xd51ea6fa85785631, 0x552a74227f3ea565, + 0x8533285c936b35de, 0xd53a88958f87275f, + 0xa67ff273b8460356, 0x8a892abaf368f137, + 0xd01fef10a657842c, 0x2d2b7569b0432d85, + 0x8213f56a67f6b29b, 0x9c3b29620e29fc73, + 0xa298f2c501f45f42, 0x8349f3ba91b47b8f, + 0xcb3f2f7642717713, 0x241c70a936219a73, + 0xfe0efb53d30dd4d7, 0xed238cd383aa0110, + 0x9ec95d1463e8a506, 0xf4363804324a40aa, + 0xc67bb4597ce2ce48, 0xb143c6053edcd0d5, + 0xf81aa16fdc1b81da, 0xdd94b7868e94050a, + 0x9b10a4e5e9913128, 0xca7cf2b4191c8326, + 0xc1d4ce1f63f57d72, 0xfd1c2f611f63a3f0, + 0xf24a01a73cf2dccf, 0xbc633b39673c8cec, + 0x976e41088617ca01, 0xd5be0503e085d813, + 0xbd49d14aa79dbc82, 0x4b2d8644d8a74e18, + 0xec9c459d51852ba2, 0xddf8e7d60ed1219e, + 0x93e1ab8252f33b45, 0xcabb90e5c942b503, + 0xb8da1662e7b00a17, 0x3d6a751f3b936243, + 0xe7109bfba19c0c9d, 0xcc512670a783ad4, + 0x906a617d450187e2, 0x27fb2b80668b24c5, + 0xb484f9dc9641e9da, 0xb1f9f660802dedf6, + 0xe1a63853bbd26451, 0x5e7873f8a0396973, + 0x8d07e33455637eb2, 0xdb0b487b6423e1e8, + 0xb049dc016abc5e5f, 0x91ce1a9a3d2cda62, + 0xdc5c5301c56b75f7, 0x7641a140cc7810fb, + 0x89b9b3e11b6329ba, 0xa9e904c87fcb0a9d, + 0xac2820d9623bf429, 0x546345fa9fbdcd44, + 0xd732290fbacaf133, 0xa97c177947ad4095, + 0x867f59a9d4bed6c0, 0x49ed8eabcccc485d, + 0xa81f301449ee8c70, 0x5c68f256bfff5a74, + 0xd226fc195c6a2f8c, 0x73832eec6fff3111, + 0x83585d8fd9c25db7, 0xc831fd53c5ff7eab, + 0xa42e74f3d032f525, 0xba3e7ca8b77f5e55, + 0xcd3a1230c43fb26f, 0x28ce1bd2e55f35eb, + 0x80444b5e7aa7cf85, 0x7980d163cf5b81b3, + 0xa0555e361951c366, 0xd7e105bcc332621f, + 0xc86ab5c39fa63440, 0x8dd9472bf3fefaa7, + 0xfa856334878fc150, 0xb14f98f6f0feb951, + 0x9c935e00d4b9d8d2, 0x6ed1bf9a569f33d3, + 0xc3b8358109e84f07, 0xa862f80ec4700c8, + 0xf4a642e14c6262c8, 0xcd27bb612758c0fa, + 0x98e7e9cccfbd7dbd, 0x8038d51cb897789c, + 0xbf21e44003acdd2c, 0xe0470a63e6bd56c3, + 0xeeea5d5004981478, 0x1858ccfce06cac74, + 0x95527a5202df0ccb, 0xf37801e0c43ebc8, + 0xbaa718e68396cffd, 0xd30560258f54e6ba, + 0xe950df20247c83fd, 0x47c6b82ef32a2069, + 0x91d28b7416cdd27e, 0x4cdc331d57fa5441, + 0xb6472e511c81471d, 0xe0133fe4adf8e952, + 0xe3d8f9e563a198e5, 0x58180fddd97723a6, + 0x8e679c2f5e44ff8f, 0x570f09eaa7ea7648, + }; +}; + +#if FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE + +template +constexpr uint64_t + powers_template::power_of_five_128[number_of_entries]; + +#endif + +using powers = powers_template<>; + +} // namespace fast_float + +#endif diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/float_common.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/float_common.h new file mode 100644 index 0000000000..1070603a81 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/float_common.h @@ -0,0 +1,1419 @@ +#ifndef FASTFLOAT_FLOAT_COMMON_H +#define FASTFLOAT_FLOAT_COMMON_H + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef __has_include +#if __has_include() && (__cplusplus > 202002L || (defined(_MSVC_LANG) && (_MSVC_LANG > 202002L))) +#include +#endif +#endif +#include "constexpr_feature_detect.h" + +#define FASTFLOAT_VERSION_MAJOR 8 +#define FASTFLOAT_VERSION_MINOR 3 +#define FASTFLOAT_VERSION_PATCH 0 + +#define FASTFLOAT_STRINGIZE_IMPL(x) #x +#define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x) + +#define FASTFLOAT_VERSION_STR \ + FASTFLOAT_STRINGIZE(FASTFLOAT_VERSION_MAJOR) \ + "." FASTFLOAT_STRINGIZE(FASTFLOAT_VERSION_MINOR) "." FASTFLOAT_STRINGIZE( \ + FASTFLOAT_VERSION_PATCH) + +#define FASTFLOAT_VERSION \ + (FASTFLOAT_VERSION_MAJOR * 10000 + FASTFLOAT_VERSION_MINOR * 100 + \ + FASTFLOAT_VERSION_PATCH) + +namespace fast_float { + +enum class chars_format : uint64_t; + +namespace detail { +constexpr chars_format basic_json_fmt = chars_format(1 << 5); +constexpr chars_format basic_fortran_fmt = chars_format(1 << 6); +constexpr chars_format basic_javascript_fmt = chars_format(1 << 9); +constexpr chars_format basic_javascript_sloppy_fmt = chars_format(1 << 10); +} // namespace detail + +enum class chars_format : uint64_t { + scientific = 1 << 0, + fixed = 1 << 2, + hex = 1 << 3, + no_infnan = 1 << 4, + // RFC 8259: https://datatracker.ietf.org/doc/html/rfc8259#section-6 + json = uint64_t(detail::basic_json_fmt) | fixed | scientific | no_infnan, + // Extension of RFC 8259 where, e.g., "inf" and "nan" are allowed. + json_or_infnan = uint64_t(detail::basic_json_fmt) | fixed | scientific, + fortran = uint64_t(detail::basic_fortran_fmt) | fixed | scientific, + general = fixed | scientific, + allow_leading_plus = 1 << 7, + skip_white_space = 1 << 8, + // ECMAScript DecimalLiteral (strict mode): + // https://tc39.es/ecma262/#prod-DecimalLiteral + // Like JSON, the integer part must not have leading zeros ("01" is + // rejected), but unlike JSON the integer part may be empty (".5") and the + // fractional part may be empty ("5.", "5.e3"). A leading minus sign is + // accepted, and a leading plus sign with allow_leading_plus. + javascript = + uint64_t(detail::basic_javascript_fmt) | fixed | scientific | no_infnan, + // ECMAScript DecimalLiteral in sloppy mode, which adds Annex B's + // NonOctalDecimalIntegerLiteral: a leading zero is accepted when one of + // the digits is 8 or 9 ("08.5" is 8.5). A leading zero followed only by + // octal digits is a LegacyOctalIntegerLiteral ("0775"): it is not a + // decimal number, so it is rejected with legacy_octal_integer_part and the + // caller may parse it in base 8. + // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals + javascript_sloppy = uint64_t(detail::basic_javascript_fmt) | + uint64_t(detail::basic_javascript_sloppy_fmt) | fixed | + scientific | no_infnan, +}; + +template struct from_chars_result_t { + UC const *ptr; + std::errc ec; + + // https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2497r0.html + constexpr explicit operator bool() const noexcept { + return ec == std::errc(); + } +}; + +using from_chars_result = from_chars_result_t; + +template struct parse_options_t { + constexpr explicit parse_options_t(chars_format fmt = chars_format::general, + UC dot = UC('.'), int b = 10) + : format(fmt), decimal_point(dot), base(b) {} + + /** Which number formats are accepted */ + chars_format format; + /** The character used as decimal point */ + UC decimal_point; + /** The base used for integers */ + int base; +}; + +using parse_options = parse_options_t; + +} // namespace fast_float + +#if FASTFLOAT_HAS_BIT_CAST +#include +#endif + +#if (defined(__x86_64) || defined(__x86_64__) || defined(_M_X64) || \ + defined(__amd64) || defined(__aarch64__) || defined(_M_ARM64) || \ + defined(__MINGW64__) || defined(__s390x__) || \ + (defined(__ppc64__) || defined(__PPC64__) || defined(__ppc64le__) || \ + defined(__PPC64LE__)) || \ + defined(__loongarch64) || (defined(__riscv) && __riscv_xlen == 64)) +#define FASTFLOAT_64BIT 1 +#elif (defined(__i386) || defined(__i386__) || defined(_M_IX86) || \ + defined(__arm__) || defined(_M_ARM) || defined(__ppc__) || \ + defined(__MINGW32__) || defined(__EMSCRIPTEN__) || \ + (defined(__riscv) && __riscv_xlen == 32)) +#define FASTFLOAT_32BIT 1 +#else + // Need to check incrementally, since SIZE_MAX is a size_t, avoid overflow. +// We can never tell the register width, but the SIZE_MAX is a good +// approximation. UINTPTR_MAX and INTPTR_MAX are optional, so avoid them for max +// portability. +#if SIZE_MAX == 0xffff +#error Unknown platform (16-bit, unsupported) +#elif SIZE_MAX == 0xffffffff +#define FASTFLOAT_32BIT 1 +#elif SIZE_MAX == 0xffffffffffffffff +#define FASTFLOAT_64BIT 1 +#else +#error Unknown platform (not 32-bit, not 64-bit?) +#endif +#endif + +#if ((defined(_WIN32) || defined(_WIN64)) && !defined(__clang__)) || \ + (defined(_M_ARM64) && !defined(__MINGW32__)) +#include +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +#define FASTFLOAT_VISUAL_STUDIO 1 +#endif + +#if defined __BYTE_ORDER__ && defined __ORDER_BIG_ENDIAN__ +#define FASTFLOAT_IS_BIG_ENDIAN (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +#elif defined _WIN32 +#define FASTFLOAT_IS_BIG_ENDIAN 0 +#else +#if defined(__APPLE__) || defined(__FreeBSD__) +#include +#elif defined(sun) || defined(__sun) +#include +#elif defined(__MVS__) +#include +#else +#ifdef __has_include +#if __has_include() +#include +#endif //__has_include() +#endif //__has_include +#endif +# +#ifndef __BYTE_ORDER__ +// safe choice +#define FASTFLOAT_IS_BIG_ENDIAN 0 +#endif +# +#ifndef __ORDER_LITTLE_ENDIAN__ +// safe choice +#define FASTFLOAT_IS_BIG_ENDIAN 0 +#endif +# +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define FASTFLOAT_IS_BIG_ENDIAN 0 +#else +#define FASTFLOAT_IS_BIG_ENDIAN 1 +#endif +#endif + +#if defined(__SSE2__) || (defined(FASTFLOAT_VISUAL_STUDIO) && \ + (defined(_M_AMD64) || defined(_M_X64) || \ + (defined(_M_IX86_FP) && _M_IX86_FP == 2))) +#define FASTFLOAT_SSE2 1 +#endif + +#if defined(__aarch64__) || defined(_M_ARM64) +#define FASTFLOAT_NEON 1 +#endif + +#if defined(FASTFLOAT_SSE2) || defined(FASTFLOAT_NEON) +#define FASTFLOAT_HAS_SIMD 1 +#endif + +#if defined(__GNUC__) +// disable -Wcast-align=strict (GCC only) +#define FASTFLOAT_SIMD_DISABLE_WARNINGS \ + _Pragma("GCC diagnostic push") \ + _Pragma("GCC diagnostic ignored \"-Wcast-align\"") +#else +#define FASTFLOAT_SIMD_DISABLE_WARNINGS +#endif + +#if defined(__GNUC__) +#define FASTFLOAT_SIMD_RESTORE_WARNINGS _Pragma("GCC diagnostic pop") +#else +#define FASTFLOAT_SIMD_RESTORE_WARNINGS +#endif + +#ifdef FASTFLOAT_VISUAL_STUDIO +#define fastfloat_really_inline __forceinline +#else +#define fastfloat_really_inline inline __attribute__((always_inline)) +#endif + +// Opposite of fastfloat_really_inline. Used for a rare, format-specific variant +// of a force-inlined function, so that its body does not land in the frame of +// every caller that will never execute it. +#ifdef FASTFLOAT_VISUAL_STUDIO +#define fastfloat_never_inline __declspec(noinline) +#elif defined(__GNUC__) || defined(__clang__) +#define fastfloat_never_inline __attribute__((noinline)) +#else +#define fastfloat_never_inline +#endif + +// fastfloat_really_inline under clang only, a no-op elsewhere. Clang declines +// to inline the parser into its callers, so the parser instantiation with a +// compile-time format (from_chars_fixed_format) must force the one thin +// forwarder between it and the parser body. GCC inlines the chain on its own, +// and forcing it there changes its inlining order for the worse. +#ifdef __clang__ +#define fastfloat_clang_really_inline fastfloat_really_inline +#else +#define fastfloat_clang_really_inline +#endif + +// fastfloat_unlikely under clang only, a plain condition elsewhere. Clang +// if-converts rare checks into chains of conditional moves that every +// conversion executes; the hint keeps them as branches. GCC already emits +// branches there, and the hint only reorders its blocks, for the worse. +#ifdef __clang__ +#define fastfloat_clang_unlikely(x) fastfloat_unlikely(x) +#else +#define fastfloat_clang_unlikely(x) (x) +#endif + +// Branch-probability hint marking the rare slow-path branches as cold, so the +// optimizer keeps the out-of-line slow-path re-parse off the hot path (and does +// not duplicate the force-inlined hot scanner into the caller, which bloated +// the hot frame and hurt ILP on some targets). Used at the call site as +// if fastfloat_unlikely(cond) { ... } +// (the macro supplies the parentheses). It expands to the standard [[unlikely]] +// attribute when supported, otherwise to __builtin_expect on GCC/Clang, or +// to a no-op elsewhere (e.g. pre-C++20 MSVC, which has no equivalent hint). +#ifdef __has_cpp_attribute +#if __has_cpp_attribute(unlikely) >= 201803L +// g++-9 hits hits this branch, but then fails to compile +// [[unlikely]]. This happens only with g++-9. +#if !defined(__GNUC__) || (__GNUC__ != 9) +#define FASTFLOAT_USE_UNLIKELY_ATTR +#endif +#endif +#endif + +#ifdef FASTFLOAT_USE_UNLIKELY_ATTR +#define fastfloat_unlikely(x) (x) [[unlikely]] +#elif defined(__GNUC__) || defined(__clang__) +#define fastfloat_unlikely(x) (__builtin_expect(!!(x), 0)) +#else +#define fastfloat_unlikely(x) (x) +#endif + +#ifndef FASTFLOAT_ASSERT +#define FASTFLOAT_ASSERT(x) \ + { static_cast(x); } +#endif + +#ifndef FASTFLOAT_DEBUG_ASSERT +#define FASTFLOAT_DEBUG_ASSERT(x) \ + { static_cast(x); } +#endif + +// rust style `try!()` macro, or `?` operator +#define FASTFLOAT_TRY(x) \ + { \ + if (!(x)) \ + return false; \ + } + +#define FASTFLOAT_ENABLE_IF(...) \ + typename std::enable_if<(__VA_ARGS__), int>::type + +namespace fast_float { + +fastfloat_really_inline constexpr bool cpp20_and_in_constexpr() { +#if FASTFLOAT_HAS_IS_CONSTANT_EVALUATED + return std::is_constant_evaluated(); +#else + return false; +#endif +} + +template +struct is_supported_float_type + : std::integral_constant< + bool, std::is_same::value || std::is_same::value +#ifdef __STDCPP_FLOAT64_T__ + || std::is_same::value +#endif +#ifdef __STDCPP_FLOAT32_T__ + || std::is_same::value +#endif +#ifdef __STDCPP_FLOAT16_T__ + || std::is_same::value +#endif +#ifdef __STDCPP_BFLOAT16_T__ + || std::is_same::value +#endif + > { +}; + +template +using equiv_uint_t = typename std::conditional< + sizeof(T) == 1, uint8_t, + typename std::conditional< + sizeof(T) == 2, uint16_t, + typename std::conditional::type>::type>::type; + +template struct is_supported_integer_type : std::is_integral {}; + +template +struct is_supported_char_type + : std::integral_constant::value || + std::is_same::value || + std::is_same::value || + std::is_same::value +#ifdef __cpp_char8_t + || std::is_same::value +#endif + > { +}; + +// Compares two ASCII strings in a case insensitive manner. The expected +// string is lowercase ASCII, so OR-ing 0x20 into each actual character maps +// 'A'..'Z' onto 'a'..'z' and leaves everything else mismatched. The lengths +// used are tiny constants (3 and 5), so the compiler unrolls this loop; keep +// it small so that the (rarely executed) inf/nan path does not bloat the hot +// parser it is inlined into. +template +inline FASTFLOAT_CONSTEXPR14 bool +fastfloat_strncasecmp(UC const *actual_mixedcase, UC const *expected_lowercase, + size_t length) { + for (size_t i = 0; i < length; ++i) { + if ((actual_mixedcase[i] | 32) != expected_lowercase[i]) { + return false; + } + } + return true; +} + +#ifndef FLT_EVAL_METHOD +#error "FLT_EVAL_METHOD should be defined, please include cfloat." +#endif + +// a pointer and a length to a contiguous block of memory +template struct span { + T const *ptr; + size_t length; + + constexpr span(T const *_ptr, size_t _length) : ptr(_ptr), length(_length) {} + + constexpr span() : ptr(nullptr), length(0) {} + + constexpr size_t len() const noexcept { return length; } + + FASTFLOAT_CONSTEXPR14 const T &operator[](size_t index) const noexcept { + FASTFLOAT_DEBUG_ASSERT(index < length); + return ptr[index]; + } +}; + +struct value128 { + uint64_t low; + uint64_t high; + + constexpr value128(uint64_t _low, uint64_t _high) : low(_low), high(_high) {} + + constexpr value128() : low(0), high(0) {} +}; + +/* Helper C++14 constexpr generic implementation of leading_zeroes */ +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 int +leading_zeroes_generic(uint64_t input_num, int last_bit = 0) { + if (input_num & uint64_t(0xffffffff00000000)) { + input_num >>= 32; + last_bit |= 32; + } + if (input_num & uint64_t(0xffff0000)) { + input_num >>= 16; + last_bit |= 16; + } + if (input_num & uint64_t(0xff00)) { + input_num >>= 8; + last_bit |= 8; + } + if (input_num & uint64_t(0xf0)) { + input_num >>= 4; + last_bit |= 4; + } + if (input_num & uint64_t(0xc)) { + input_num >>= 2; + last_bit |= 2; + } + if (input_num & uint64_t(0x2)) { /* input_num >>= 1; */ + last_bit |= 1; + } + return 63 - last_bit; +} + +/* result might be undefined when input_num is zero */ +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 int +leading_zeroes(uint64_t input_num) { + assert(input_num > 0); + if (cpp20_and_in_constexpr()) { + return leading_zeroes_generic(input_num); + } +#ifdef FASTFLOAT_VISUAL_STUDIO +#if defined(_M_X64) || defined(_M_ARM64) + unsigned long leading_zero = 0; + // Search the mask data from most significant bit (MSB) + // to least significant bit (LSB) for a set bit (1). + _BitScanReverse64(&leading_zero, input_num); + return static_cast(63 - leading_zero); +#else + return leading_zeroes_generic(input_num); +#endif +#else + return __builtin_clzll(input_num); +#endif +} + +/* Helper C++14 constexpr generic implementation of countr_zero for 32-bit */ +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 int +countr_zero_generic_32(uint32_t input_num) { + if (input_num == 0) { + return 32; + } + int last_bit = 0; + if (!(input_num & 0x0000FFFF)) { + input_num >>= 16; + last_bit |= 16; + } + if (!(input_num & 0x00FF)) { + input_num >>= 8; + last_bit |= 8; + } + if (!(input_num & 0x0F)) { + input_num >>= 4; + last_bit |= 4; + } + if (!(input_num & 0x3)) { + input_num >>= 2; + last_bit |= 2; + } + if (!(input_num & 0x1)) { + last_bit |= 1; + } + return last_bit; +} + +/* count trailing zeroes for 32-bit integers */ +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 int +countr_zero_32(uint32_t input_num) { + if (cpp20_and_in_constexpr()) { + return countr_zero_generic_32(input_num); + } +#ifdef FASTFLOAT_VISUAL_STUDIO + unsigned long trailing_zero = 0; + if (_BitScanForward(&trailing_zero, input_num)) { + return static_cast(trailing_zero); + } + return 32; +#else + return input_num == 0 ? 32 : __builtin_ctz(input_num); +#endif +} + +// slow emulation routine for 32-bit +fastfloat_really_inline constexpr uint64_t emulu(uint32_t x, uint32_t y) { + return x * static_cast(y); +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 uint64_t +umul128_generic(uint64_t ab, uint64_t cd, uint64_t *hi) { + uint64_t ad = + emulu(static_cast(ab >> 32), static_cast(cd)); + uint64_t bd = emulu(static_cast(ab), static_cast(cd)); + uint64_t adbc = + ad + emulu(static_cast(ab), static_cast(cd >> 32)); + uint64_t adbc_carry = static_cast(adbc < ad); + uint64_t lo = bd + (adbc << 32); + *hi = + emulu(static_cast(ab >> 32), static_cast(cd >> 32)) + + (adbc >> 32) + (adbc_carry << 32) + static_cast(lo < bd); + return lo; +} + +#ifdef FASTFLOAT_32BIT + +// slow emulation routine for 32-bit +#if !defined(__MINGW64__) +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 uint64_t _umul128(uint64_t ab, + uint64_t cd, + uint64_t *hi) { + return umul128_generic(ab, cd, hi); +} +#endif // !__MINGW64__ + +#endif // FASTFLOAT_32BIT + +// compute 64-bit a*b +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 value128 +full_multiplication(uint64_t a, uint64_t b) { + if (cpp20_and_in_constexpr()) { + value128 answer; + answer.low = umul128_generic(a, b, &answer.high); + return answer; + } + value128 answer; +#if defined(_M_ARM64) && !defined(__MINGW32__) + // ARM64 has native support for 64-bit multiplications, no need to emulate + // But MinGW on ARM64 doesn't have native support for 64-bit multiplications + answer.high = __umulh(a, b); + answer.low = a * b; +#elif defined(FASTFLOAT_32BIT) || (defined(_WIN64) && !defined(__clang__) && \ + !defined(_M_ARM64) && !defined(__GNUC__)) + answer.low = _umul128(a, b, &answer.high); // _umul128 not available on ARM64 +#elif defined(FASTFLOAT_64BIT) && defined(__SIZEOF_INT128__) + __uint128_t r = static_cast<__uint128_t>(a) * b; + answer.low = uint64_t(r); + answer.high = uint64_t(r >> 64); +#else + answer.low = umul128_generic(a, b, &answer.high); +#endif + return answer; +} + +struct adjusted_mantissa { + uint64_t mantissa{0}; + int32_t power2{0}; // a negative value indicates an invalid result + adjusted_mantissa() = default; + + constexpr bool operator==(adjusted_mantissa const &o) const { + return mantissa == o.mantissa && power2 == o.power2; + } + + constexpr bool operator!=(adjusted_mantissa const &o) const { + return mantissa != o.mantissa || power2 != o.power2; + } +}; + +// Bias so we can get the real exponent with an invalid adjusted_mantissa. +constexpr static int32_t invalid_am_bias = -0x8000; + +// used for binary_format_lookup_tables::max_mantissa +constexpr uint64_t constant_55555 = 5 * 5 * 5 * 5 * 5; + +template struct binary_format_lookup_tables; + +template struct binary_format : binary_format_lookup_tables { + using equiv_uint = equiv_uint_t; + + static constexpr int mantissa_explicit_bits(); + static constexpr int minimum_exponent(); + static constexpr int infinite_power(); + static constexpr int sign_index(); + static constexpr int + min_exponent_fast_path(); // used when fegetround() == FE_TONEAREST + static constexpr int max_exponent_fast_path(); + static constexpr int max_exponent_round_to_even(); + static constexpr int min_exponent_round_to_even(); + static constexpr uint64_t max_mantissa_fast_path(int64_t power); + static constexpr uint64_t + max_mantissa_fast_path(); // used when fegetround() == FE_TONEAREST + static constexpr bool fast_path_can_overflow(); + static constexpr bool subnormal_ties_possible(); + static constexpr int largest_power_of_ten(); + static constexpr int smallest_power_of_ten(); + static constexpr T exact_power_of_ten(int64_t power); + static constexpr size_t max_digits(); + static constexpr equiv_uint exponent_mask(); + static constexpr equiv_uint mantissa_mask(); + static constexpr equiv_uint hidden_bit_mask(); +}; + +template struct binary_format_lookup_tables { + static constexpr double powers_of_ten[] = { + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, + 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22}; + + // Largest integer value v so that (5**index * v) <= 1<<53. + // 0x20000000000000 == 1 << 53 + static constexpr uint64_t max_mantissa[] = { + 0x20000000000000, + 0x20000000000000 / 5, + 0x20000000000000 / (5 * 5), + 0x20000000000000 / (5 * 5 * 5), + 0x20000000000000 / (5 * 5 * 5 * 5), + 0x20000000000000 / (constant_55555), + 0x20000000000000 / (constant_55555 * 5), + 0x20000000000000 / (constant_55555 * 5 * 5), + 0x20000000000000 / (constant_55555 * 5 * 5 * 5), + 0x20000000000000 / (constant_55555 * 5 * 5 * 5 * 5), + 0x20000000000000 / (constant_55555 * constant_55555), + 0x20000000000000 / (constant_55555 * constant_55555 * 5), + 0x20000000000000 / (constant_55555 * constant_55555 * 5 * 5), + 0x20000000000000 / (constant_55555 * constant_55555 * 5 * 5 * 5), + 0x20000000000000 / (constant_55555 * constant_55555 * 5 * 5 * 5 * 5), + 0x20000000000000 / (constant_55555 * constant_55555 * constant_55555), + 0x20000000000000 / (constant_55555 * constant_55555 * constant_55555 * 5), + 0x20000000000000 / + (constant_55555 * constant_55555 * constant_55555 * 5 * 5), + 0x20000000000000 / + (constant_55555 * constant_55555 * constant_55555 * 5 * 5 * 5), + 0x20000000000000 / + (constant_55555 * constant_55555 * constant_55555 * 5 * 5 * 5 * 5), + 0x20000000000000 / + (constant_55555 * constant_55555 * constant_55555 * constant_55555), + 0x20000000000000 / (constant_55555 * constant_55555 * constant_55555 * + constant_55555 * 5), + 0x20000000000000 / (constant_55555 * constant_55555 * constant_55555 * + constant_55555 * 5 * 5), + 0x20000000000000 / (constant_55555 * constant_55555 * constant_55555 * + constant_55555 * 5 * 5 * 5)}; +}; + +#if FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE + +template +constexpr double binary_format_lookup_tables::powers_of_ten[]; + +template +constexpr uint64_t binary_format_lookup_tables::max_mantissa[]; + +#endif + +template struct binary_format_lookup_tables { + static constexpr float powers_of_ten[] = {1e0f, 1e1f, 1e2f, 1e3f, 1e4f, 1e5f, + 1e6f, 1e7f, 1e8f, 1e9f, 1e10f}; + + // Largest integer value v so that (5**index * v) <= 1<<24. + // 0x1000000 == 1<<24 + static constexpr uint64_t max_mantissa[] = { + 0x1000000, + 0x1000000 / 5, + 0x1000000 / (5 * 5), + 0x1000000 / (5 * 5 * 5), + 0x1000000 / (5 * 5 * 5 * 5), + 0x1000000 / (constant_55555), + 0x1000000 / (constant_55555 * 5), + 0x1000000 / (constant_55555 * 5 * 5), + 0x1000000 / (constant_55555 * 5 * 5 * 5), + 0x1000000 / (constant_55555 * 5 * 5 * 5 * 5), + 0x1000000 / (constant_55555 * constant_55555), + 0x1000000 / (constant_55555 * constant_55555 * 5)}; +}; + +#if FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE + +template +constexpr float binary_format_lookup_tables::powers_of_ten[]; + +template +constexpr uint64_t binary_format_lookup_tables::max_mantissa[]; + +#endif + +template <> +inline constexpr int binary_format::min_exponent_fast_path() { +#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0) + return 0; +#else + return -22; +#endif +} + +template <> +inline constexpr int binary_format::min_exponent_fast_path() { +#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0) + return 0; +#else + return -10; +#endif +} + +template <> +inline constexpr int binary_format::mantissa_explicit_bits() { + return 52; +} + +template <> +inline constexpr int binary_format::mantissa_explicit_bits() { + return 23; +} + +template <> +inline constexpr int binary_format::max_exponent_round_to_even() { + return 23; +} + +template <> +inline constexpr int binary_format::max_exponent_round_to_even() { + return 10; +} + +template <> +inline constexpr int binary_format::min_exponent_round_to_even() { + return -4; +} + +template <> +inline constexpr int binary_format::min_exponent_round_to_even() { + return -17; +} + +template <> inline constexpr int binary_format::minimum_exponent() { + return -1023; +} + +template <> inline constexpr int binary_format::minimum_exponent() { + return -127; +} + +template <> inline constexpr int binary_format::infinite_power() { + return 0x7FF; +} + +template <> inline constexpr int binary_format::infinite_power() { + return 0xFF; +} + +template <> inline constexpr int binary_format::sign_index() { + return 63; +} + +template <> inline constexpr int binary_format::sign_index() { + return 31; +} + +template <> +inline constexpr int binary_format::max_exponent_fast_path() { + return 22; +} + +template <> +inline constexpr int binary_format::max_exponent_fast_path() { + return 10; +} + +template <> +inline constexpr uint64_t binary_format::max_mantissa_fast_path() { + return uint64_t(2) << mantissa_explicit_bits(); +} + +template <> +inline constexpr uint64_t binary_format::max_mantissa_fast_path() { + return uint64_t(2) << mantissa_explicit_bits(); +} + +// credit: Jakub Jelínek +#ifdef __STDCPP_FLOAT16_T__ +template struct binary_format_lookup_tables { + static constexpr std::float16_t powers_of_ten[] = {1e0f16, 1e1f16, 1e2f16, + 1e3f16, 1e4f16}; + + // Largest integer value v so that (5**index * v) <= 1<<11. + // 0x800 == 1<<11 + static constexpr uint64_t max_mantissa[] = {0x800, + 0x800 / 5, + 0x800 / (5 * 5), + 0x800 / (5 * 5 * 5), + 0x800 / (5 * 5 * 5 * 5), + 0x800 / (constant_55555)}; +}; + +#if FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE + +template +constexpr std::float16_t + binary_format_lookup_tables::powers_of_ten[]; + +template +constexpr uint64_t + binary_format_lookup_tables::max_mantissa[]; + +#endif + +template <> +inline constexpr std::float16_t +binary_format::exact_power_of_ten(int64_t power) { + // Work around clang bug https://godbolt.org/z/zedh7rrhc + return static_cast(powers_of_ten[0]), powers_of_ten[power]; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::exponent_mask() { + return 0x7C00; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::mantissa_mask() { + return 0x03FF; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::hidden_bit_mask() { + return 0x0400; +} + +template <> +inline constexpr int binary_format::max_exponent_fast_path() { + return 4; +} + +template <> +inline constexpr int binary_format::mantissa_explicit_bits() { + return 10; +} + +template <> +inline constexpr uint64_t +binary_format::max_mantissa_fast_path() { + return uint64_t(2) << mantissa_explicit_bits(); +} + +template <> +inline constexpr uint64_t +binary_format::max_mantissa_fast_path(int64_t power) { + // caller is responsible to ensure that + // power >= 0 && power <= 4 + // + // Work around clang bug https://godbolt.org/z/zedh7rrhc + return static_cast(max_mantissa[0]), max_mantissa[power]; +} + +template <> +inline constexpr int binary_format::min_exponent_fast_path() { + // w / 10^k with w <= 2^11 and k <= 4 rounds correctly even when evaluated + // in float or double first (checked in script/format_parameters.py). + return -4; +} + +template <> +inline constexpr int +binary_format::max_exponent_round_to_even() { + return 5; +} + +template <> +inline constexpr int +binary_format::min_exponent_round_to_even() { + // -22 covers the normal ties; subnormal ties such as + // 2^-25 = 298023223876953125e-25 need q = -25 and q = -26. + return -26; +} + +template <> +inline constexpr int binary_format::minimum_exponent() { + return -15; +} + +template <> +inline constexpr int binary_format::infinite_power() { + return 0x1F; +} + +template <> inline constexpr int binary_format::sign_index() { + return 15; +} + +template <> +inline constexpr int binary_format::largest_power_of_ten() { + return 4; +} + +template <> +inline constexpr int binary_format::smallest_power_of_ten() { + // (10^19 - 1) * 10^-27 < 2^-25, so any q < -26 rounds to zero. + return -26; +} + +template <> +inline constexpr size_t binary_format::max_digits() { + return 22; +} +#endif // __STDCPP_FLOAT16_T__ + +// credit: Jakub Jelínek +#ifdef __STDCPP_BFLOAT16_T__ +template struct binary_format_lookup_tables { + static constexpr std::bfloat16_t powers_of_ten[] = {1e0bf16, 1e1bf16, 1e2bf16, + 1e3bf16}; + + // Largest integer value v so that (5**index * v) <= 1<<8. + // 0x100 == 1<<8 + static constexpr uint64_t max_mantissa[] = {0x100, 0x100 / 5, 0x100 / (5 * 5), + 0x100 / (5 * 5 * 5), + 0x100 / (5 * 5 * 5 * 5)}; +}; + +#if FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE + +template +constexpr std::bfloat16_t + binary_format_lookup_tables::powers_of_ten[]; + +template +constexpr uint64_t + binary_format_lookup_tables::max_mantissa[]; + +#endif + +template <> +inline constexpr std::bfloat16_t +binary_format::exact_power_of_ten(int64_t power) { + // Work around clang bug https://godbolt.org/z/zedh7rrhc + return static_cast(powers_of_ten[0]), powers_of_ten[power]; +} + +template <> +inline constexpr int binary_format::max_exponent_fast_path() { + return 3; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::exponent_mask() { + return 0x7F80; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::mantissa_mask() { + return 0x007F; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::hidden_bit_mask() { + return 0x0080; +} + +template <> +inline constexpr int binary_format::mantissa_explicit_bits() { + return 7; +} + +template <> +inline constexpr uint64_t +binary_format::max_mantissa_fast_path() { + return uint64_t(2) << mantissa_explicit_bits(); +} + +template <> +inline constexpr uint64_t +binary_format::max_mantissa_fast_path(int64_t power) { + // caller is responsible to ensure that + // power >= 0 && power <= 3 + // + // Work around clang bug https://godbolt.org/z/zedh7rrhc + return static_cast(max_mantissa[0]), max_mantissa[power]; +} + +template <> +inline constexpr int binary_format::min_exponent_fast_path() { + // Same argument as for std::float16_t (w <= 2^8, k <= 3). + return -3; +} + +template <> +inline constexpr int +binary_format::max_exponent_round_to_even() { + return 3; +} + +template <> +inline constexpr int +binary_format::min_exponent_round_to_even() { + return -24; +} + +template <> +inline constexpr int binary_format::minimum_exponent() { + return -127; +} + +template <> +inline constexpr int binary_format::infinite_power() { + return 0xFF; +} + +template <> inline constexpr int binary_format::sign_index() { + return 15; +} + +template <> +inline constexpr int binary_format::largest_power_of_ten() { + return 38; +} + +template <> +inline constexpr int binary_format::smallest_power_of_ten() { + // (10^19 - 1) * 10^-60 < 2^-134, so any q < -59 rounds to zero. + return -59; +} + +template <> +inline constexpr size_t binary_format::max_digits() { + return 98; +} +#endif // __STDCPP_BFLOAT16_T__ + +// Whether Clinger's fast path can overflow: only for std::float16_t, where +// 2^11 * 10^4 > 65504. +template +inline constexpr bool binary_format::fast_path_can_overflow() { + return double(max_mantissa_fast_path()) * + double(exact_power_of_ten(max_exponent_fast_path())) > + double((std::numeric_limits::max)()); +} + +// A subnormal needs w * 10^q < 2^(minimum_exponent() + 1), so q is at most +// (minimum_exponent() + 1) * log10(2), with 1233/4096 < log10(2). Only +// std::float16_t has such q in its round-to-even range. We compare +// 4096 * q with (minimum_exponent() + 1) * 1233 to avoid right-shifting a +// negative value (implementation-defined before C++20). +template +inline constexpr bool binary_format::subnormal_ties_possible() { + return min_exponent_round_to_even() * 4096 <= (minimum_exponent() + 1) * 1233; +} + +template <> +inline constexpr uint64_t +binary_format::max_mantissa_fast_path(int64_t power) { + // caller is responsible to ensure that + // power >= 0 && power <= 22 + // + // Work around clang bug https://godbolt.org/z/zedh7rrhc + return static_cast(max_mantissa[0]), max_mantissa[power]; +} + +template <> +inline constexpr uint64_t +binary_format::max_mantissa_fast_path(int64_t power) { + // caller is responsible to ensure that + // power >= 0 && power <= 10 + // + // Work around clang bug https://godbolt.org/z/zedh7rrhc + return static_cast(max_mantissa[0]), max_mantissa[power]; +} + +template <> +inline constexpr double +binary_format::exact_power_of_ten(int64_t power) { + // Work around clang bug https://godbolt.org/z/zedh7rrhc + return static_cast(powers_of_ten[0]), powers_of_ten[power]; +} + +template <> +inline constexpr float binary_format::exact_power_of_ten(int64_t power) { + // Work around clang bug https://godbolt.org/z/zedh7rrhc + return static_cast(powers_of_ten[0]), powers_of_ten[power]; +} + +template <> inline constexpr int binary_format::largest_power_of_ten() { + return 308; +} + +template <> inline constexpr int binary_format::largest_power_of_ten() { + return 38; +} + +template <> +inline constexpr int binary_format::smallest_power_of_ten() { + return -342; +} + +template <> inline constexpr int binary_format::smallest_power_of_ten() { + return -64; +} + +template <> inline constexpr size_t binary_format::max_digits() { + return 769; +} + +template <> inline constexpr size_t binary_format::max_digits() { + return 114; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::exponent_mask() { + return 0x7F800000; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::exponent_mask() { + return 0x7FF0000000000000; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::mantissa_mask() { + return 0x007FFFFF; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::mantissa_mask() { + return 0x000FFFFFFFFFFFFF; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::hidden_bit_mask() { + return 0x00800000; +} + +template <> +inline constexpr binary_format::equiv_uint +binary_format::hidden_bit_mask() { + return 0x0010000000000000; +} + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 void +to_float(bool negative, adjusted_mantissa am, T &value) { + using equiv_uint = equiv_uint_t; + equiv_uint word = equiv_uint(am.mantissa); + word = equiv_uint(word | equiv_uint(am.power2) + << binary_format::mantissa_explicit_bits()); + word = + equiv_uint(word | equiv_uint(negative) << binary_format::sign_index()); +#if FASTFLOAT_HAS_BIT_CAST + value = std::bit_cast(word); +#else + ::memcpy(&value, &word, sizeof(T)); +#endif +} + +template struct space_lut { + static constexpr bool value[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +}; + +#if FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE + +template constexpr bool space_lut::value[]; + +#endif + +template constexpr bool is_space(UC c) { + // wchar_t and char can be signed, so a negative code unit slips past a plain + // `c < 256` and then indexes the table by its truncated low byte. Compare as + // unsigned, matching the care taken in ch_to_digit. + using UnsignedUC = typename std::make_unsigned::type; + return static_cast(c) < 256 && space_lut<>::value[uint8_t(c)]; +} + +template static constexpr uint64_t int_cmp_zeros() { + static_assert((sizeof(UC) == 1) || (sizeof(UC) == 2) || (sizeof(UC) == 4), + "Unsupported character size"); + return (sizeof(UC) == 1) ? 0x3030303030303030 + : (sizeof(UC) == 2) + ? (uint64_t(UC('0')) << 48 | uint64_t(UC('0')) << 32 | + uint64_t(UC('0')) << 16 | UC('0')) + : (uint64_t(UC('0')) << 32 | UC('0')); +} + +template static constexpr int int_cmp_len() { + return sizeof(uint64_t) / sizeof(UC); +} + +template constexpr UC const *str_const_nan(); + +template <> constexpr char const *str_const_nan() { return "nan"; } + +template <> constexpr wchar_t const *str_const_nan() { return L"nan"; } + +template <> constexpr char16_t const *str_const_nan() { + return u"nan"; +} + +template <> constexpr char32_t const *str_const_nan() { + return U"nan"; +} + +#ifdef __cpp_char8_t +template <> constexpr char8_t const *str_const_nan() { + return u8"nan"; +} +#endif + +template constexpr UC const *str_const_inf(); + +template <> constexpr char const *str_const_inf() { return "infinity"; } + +template <> constexpr wchar_t const *str_const_inf() { + return L"infinity"; +} + +template <> constexpr char16_t const *str_const_inf() { + return u"infinity"; +} + +template <> constexpr char32_t const *str_const_inf() { + return U"infinity"; +} + +#ifdef __cpp_char8_t +template <> constexpr char8_t const *str_const_inf() { + return u8"infinity"; +} +#endif + +template struct int_luts { + static constexpr uint8_t chdigit[] = { + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, + 255, 255, 255, 255, 255, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, + 35, 255, 255, 255, 255, 255, 255, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255}; + + static constexpr size_t maxdigits_u64[] = { + 64, 41, 32, 28, 25, 23, 22, 21, 20, 19, 18, 18, 17, 17, 16, 16, 16, 16, + 15, 15, 15, 15, 14, 14, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13}; + + static constexpr uint64_t min_safe_u64[] = { + 9223372036854775808ull, 12157665459056928801ull, 4611686018427387904, + 7450580596923828125, 4738381338321616896, 3909821048582988049, + 9223372036854775808ull, 12157665459056928801ull, 10000000000000000000ull, + 5559917313492231481, 2218611106740436992, 8650415919381337933, + 2177953337809371136, 6568408355712890625, 1152921504606846976, + 2862423051509815793, 6746640616477458432, 15181127029874798299ull, + 1638400000000000000, 3243919932521508681, 6221821273427820544, + 11592836324538749809ull, 876488338465357824, 1490116119384765625, + 2481152873203736576, 4052555153018976267, 6502111422497947648, + 10260628712958602189ull, 15943230000000000000ull, 787662783788549761, + 1152921504606846976, 1667889514952984961, 2386420683693101056, + 3379220508056640625, 4738381338321616896}; +}; + +#if FASTFLOAT_DETAIL_MUST_DEFINE_CONSTEXPR_VARIABLE + +template constexpr uint8_t int_luts::chdigit[]; + +template constexpr size_t int_luts::maxdigits_u64[]; + +template constexpr uint64_t int_luts::min_safe_u64[]; + +#endif + +template +fastfloat_really_inline constexpr uint8_t ch_to_digit(UC c) { + // wchar_t and char can be signed, so we need to be careful. + using UnsignedUC = typename std::make_unsigned::type; + return int_luts<>::chdigit[static_cast( + static_cast(c) & + static_cast( + -((static_cast(c) & ~0xFFull) == 0)))]; +} + +fastfloat_really_inline constexpr size_t max_digits_u64(int base) { + return int_luts<>::maxdigits_u64[base - 2]; +} + +// If a u64 is exactly max_digits_u64() in length, this is +// the value below which it has definitely overflowed. +fastfloat_really_inline constexpr uint64_t min_safe_u64(int base) { + return int_luts<>::min_safe_u64[base - 2]; +} + +static_assert(std::is_same, uint64_t>::value, + "equiv_uint should be uint64_t for double"); +static_assert(std::numeric_limits::is_iec559, + "double must fulfill the requirements of IEC 559 (IEEE 754)"); + +static_assert(std::is_same, uint32_t>::value, + "equiv_uint should be uint32_t for float"); +static_assert(std::numeric_limits::is_iec559, + "float must fulfill the requirements of IEC 559 (IEEE 754)"); + +#ifdef __STDCPP_FLOAT64_T__ +static_assert(std::is_same, uint64_t>::value, + "equiv_uint should be uint64_t for std::float64_t"); +static_assert( + std::numeric_limits::is_iec559, + "std::float64_t must fulfill the requirements of IEC 559 (IEEE 754)"); + +template <> +struct binary_format : public binary_format {}; +#endif // __STDCPP_FLOAT64_T__ + +#ifdef __STDCPP_FLOAT32_T__ +static_assert(std::is_same, uint32_t>::value, + "equiv_uint should be uint32_t for std::float32_t"); +static_assert( + std::numeric_limits::is_iec559, + "std::float32_t must fulfill the requirements of IEC 559 (IEEE 754)"); + +template <> +struct binary_format : public binary_format {}; +#endif // __STDCPP_FLOAT32_T__ + +#ifdef __STDCPP_FLOAT16_T__ +static_assert( + std::is_same::equiv_uint, uint16_t>::value, + "equiv_uint should be uint16_t for std::float16_t"); +static_assert( + std::numeric_limits::is_iec559, + "std::float16_t must fulfill the requirements of IEC 559 (IEEE 754)"); +#endif // __STDCPP_FLOAT16_T__ + +#ifdef __STDCPP_BFLOAT16_T__ +static_assert( + std::is_same::equiv_uint, uint16_t>::value, + "equiv_uint should be uint16_t for std::bfloat16_t"); +static_assert( + std::numeric_limits::is_iec559, + "std::bfloat16_t must fulfill the requirements of IEC 559 (IEEE 754)"); +#endif // __STDCPP_BFLOAT16_T__ + +constexpr chars_format operator~(chars_format rhs) noexcept { + using int_type = std::underlying_type::type; + return static_cast(~static_cast(rhs)); +} + +constexpr chars_format operator&(chars_format lhs, chars_format rhs) noexcept { + using int_type = std::underlying_type::type; + return static_cast(static_cast(lhs) & + static_cast(rhs)); +} + +constexpr chars_format operator|(chars_format lhs, chars_format rhs) noexcept { + using int_type = std::underlying_type::type; + return static_cast(static_cast(lhs) | + static_cast(rhs)); +} + +constexpr chars_format operator^(chars_format lhs, chars_format rhs) noexcept { + using int_type = std::underlying_type::type; + return static_cast(static_cast(lhs) ^ + static_cast(rhs)); +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 chars_format & +operator&=(chars_format &lhs, chars_format rhs) noexcept { + return lhs = (lhs & rhs); +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 chars_format & +operator|=(chars_format &lhs, chars_format rhs) noexcept { + return lhs = (lhs | rhs); +} + +fastfloat_really_inline FASTFLOAT_CONSTEXPR14 chars_format & +operator^=(chars_format &lhs, chars_format rhs) noexcept { + return lhs = (lhs ^ rhs); +} + +namespace detail { +// adjust for deprecated feature macros +constexpr chars_format adjust_for_feature_macros(chars_format fmt) { + return fmt +#ifdef FASTFLOAT_ALLOWS_LEADING_PLUS + | chars_format::allow_leading_plus +#endif +#ifdef FASTFLOAT_SKIP_WHITE_SPACE + | chars_format::skip_white_space +#endif + ; +} +} // namespace detail +} // namespace fast_float + +#endif diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/parse_number.h b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/parse_number.h new file mode 100644 index 0000000000..038be5b1ca --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/cpp/third_party/fast_float/include/fast_float/parse_number.h @@ -0,0 +1,627 @@ +#ifndef FASTFLOAT_PARSE_NUMBER_H +#define FASTFLOAT_PARSE_NUMBER_H + +#include "ascii_number.h" +#include "decimal_to_binary.h" +#include "digit_comparison.h" +#include "float_common.h" + +#include +#include +#include +#include + +namespace fast_float { + +namespace detail { +/** + * Special case +inf, -inf, nan, infinity, -infinity. + * The case comparisons could be made much faster given that we know that the + * strings a null-free and fixed. + **/ +template +from_chars_result_t + FASTFLOAT_CONSTEXPR14 parse_infnan(UC const *first, UC const *last, + T &value, chars_format fmt) noexcept { + from_chars_result_t answer{}; + answer.ptr = first; + answer.ec = std::errc(); // be optimistic + // assume first < last, so dereference without checks; + bool const minusSign = (*first == UC('-')); + // C++17 20.19.3.(7.1) explicitly forbids '+' sign here + if ((*first == UC('-')) || + (uint64_t(fmt & chars_format::allow_leading_plus) && + (*first == UC('+')))) { + ++first; + } + if (last - first >= 3) { + if (fastfloat_strncasecmp(first, str_const_nan(), 3)) { + answer.ptr = (first += 3); + value = minusSign ? -std::numeric_limits::quiet_NaN() + : std::numeric_limits::quiet_NaN(); + // Check for possible nan(n-char-seq-opt), C++17 20.19.3.7, + // C11 7.20.1.3.3. At least MSVC produces nan(ind) and nan(snan). + if (first != last && *first == UC('(')) { + for (UC const *ptr = first + 1; ptr != last; ++ptr) { + if (*ptr == UC(')')) { + answer.ptr = ptr + 1; // valid nan(n-char-seq-opt) + break; + } else if (!((UC('a') <= *ptr && *ptr <= UC('z')) || + (UC('A') <= *ptr && *ptr <= UC('Z')) || + (UC('0') <= *ptr && *ptr <= UC('9')) || *ptr == UC('_'))) + break; // forbidden char, not nan(n-char-seq-opt) + } + } + return answer; + } + if (fastfloat_strncasecmp(first, str_const_inf(), 3)) { + if ((last - first >= 8) && + fastfloat_strncasecmp(first + 3, str_const_inf() + 3, 5)) { + answer.ptr = first + 8; + } else { + answer.ptr = first + 3; + } + value = minusSign ? -std::numeric_limits::infinity() + : std::numeric_limits::infinity(); + return answer; + } + } + answer.ec = std::errc::invalid_argument; + return answer; +} + +/** + * Returns true if the floating-pointing rounding mode is to 'nearest'. + * It is the default on most system. This function is meant to be inexpensive. + * Credit : @mwalcott3 + */ +fastfloat_really_inline bool rounds_to_nearest() noexcept { + // https://lemire.me/blog/2020/06/26/gcc-not-nearest/ +#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0) + return false; +#endif + // See + // A fast function to check your floating-point rounding mode + // https://lemire.me/blog/2022/11/16/a-fast-function-to-check-your-floating-point-rounding-mode/ + // + // This function is meant to be equivalent to : + // prior: #include + // return fegetround() == FE_TONEAREST; + // However, it is expected to be much faster than the fegetround() + // function call. + // + // The volatile keyword prevents the compiler from computing the function + // at compile-time. + // There might be other ways to prevent compile-time optimizations (e.g., + // asm). The value does not need to be std::numeric_limits::min(), any + // small value so that 1 + x should round to 1 would do (after accounting for + // excess precision, as in 387 instructions). + static float volatile fmin = (std::numeric_limits::min)(); + float fmini = fmin; // we copy it so that it gets loaded at most once. +// +// Explanation: +// Only when fegetround() == FE_TONEAREST do we have that +// fmin + 1.0f == 1.0f - fmin. +// +// FE_UPWARD: +// fmin + 1.0f > 1 +// 1.0f - fmin == 1 +// +// FE_DOWNWARD or FE_TOWARDZERO: +// fmin + 1.0f == 1 +// 1.0f - fmin < 1 +// +// Note: This may fail to be accurate if fast-math has been +// enabled, as rounding conventions may not apply. +#ifdef FASTFLOAT_VISUAL_STUDIO +#pragma warning(push) +// todo: is there a VS warning? +// see +// https://stackoverflow.com/questions/46079446/is-there-a-warning-for-floating-point-equality-checking-in-visual-studio-2013 +#elif defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wfloat-equal" +#elif defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + return (fmini + 1.0f == 1.0f - fmini); +#ifdef FASTFLOAT_VISUAL_STUDIO +#pragma warning(pop) +#elif defined(__clang__) +#pragma clang diagnostic pop +#elif defined(__GNUC__) +#pragma GCC diagnostic pop +#endif +} + +} // namespace detail + +template struct from_chars_caller { + template + fastfloat_clang_really_inline + FASTFLOAT_CONSTEXPR20 static from_chars_result_t + call(UC const *first, UC const *last, T &value, + parse_options_t options) noexcept { + return from_chars_advanced(first, last, value, options); + } +}; + +#ifdef __STDCPP_FLOAT32_T__ +template <> struct from_chars_caller { + template + FASTFLOAT_CONSTEXPR20 static from_chars_result_t + call(UC const *first, UC const *last, std::float32_t &value, + parse_options_t options) noexcept { + // if std::float32_t is defined, and we are in C++23 mode; macro set for + // float32; set value to float due to equivalence between float and + // float32_t + float val = 0.0f; + auto ret = from_chars_advanced(first, last, val, options); + value = val; + return ret; + } +}; +#endif + +#ifdef __STDCPP_FLOAT64_T__ +template <> struct from_chars_caller { + template + FASTFLOAT_CONSTEXPR20 static from_chars_result_t + call(UC const *first, UC const *last, std::float64_t &value, + parse_options_t options) noexcept { + // if std::float64_t is defined, and we are in C++23 mode; macro set for + // float64; set value as double due to equivalence between double and + // float64_t + double val = 0.0; + auto ret = from_chars_advanced(first, last, val, options); + value = val; + return ret; + } +}; +#endif + +#ifdef __clang__ +// Parser instantiated for a format fixed at compile time, so that every test +// on the format folds away inside it. GCC gets the same effect by inlining the +// whole parser into each caller, where the format is a constant; clang keeps +// it out of line and would otherwise re-test each format flag per conversion. +template +FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars_fixed_format(UC const *first, UC const *last, T &value) noexcept { + return from_chars_caller::call(first, last, value, + parse_options_t(Fmt)); +} +#endif + +template +FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars(UC const *first, UC const *last, T &value, + chars_format fmt /*= chars_format::general*/) noexcept { +#ifdef __clang__ + if (fmt == chars_format::general) { + return from_chars_fixed_format(first, last, + value); + } +#endif + return from_chars_caller::call(first, last, value, + parse_options_t(fmt)); +} + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 bool +clinger_fast_path_impl(uint64_t mantissa, int64_t exponent, bool is_negative, + T &value) noexcept { + // The implementation of the Clinger's fast path is convoluted because + // we want round-to-nearest in all cases, irrespective of the rounding mode + // selected on the thread. + // We proceed optimistically, assuming that detail::rounds_to_nearest() + // returns true. + if (binary_format::min_exponent_fast_path() <= exponent && + exponent <= binary_format::max_exponent_fast_path() && + mantissa <= binary_format::max_mantissa_fast_path()) { + // The mantissa bound above is a necessary condition for BOTH branches + // below: the rounding-mode-dependent branch checks the tighter + // max_mantissa_fast_path(exponent) <= max_mantissa_fast_path(). Testing + // it before detail::rounds_to_nearest() spares long-mantissa inputs + // (which can never take the fast path) the volatile-float probe. + // + // Unfortunately, the conventional Clinger's fast path is only possible + // when the system rounds to the nearest float. + // + // We expect the next branch to almost always be selected. + // We could check it first (before the previous branch), but + // there might be performance advantages at having the check + // be last. + if (!cpp20_and_in_constexpr() && detail::rounds_to_nearest()) { + // We have that fegetround() == FE_TONEAREST. + // Next is Clinger's fast path. + value = T(mantissa); + if (exponent < 0) { + value = value / binary_format::exact_power_of_ten(-exponent); + } else { + value = value * binary_format::exact_power_of_ten(exponent); + // Only std::float16_t can overflow here (e.g., "656e2"); let the + // slow path report result_out_of_range. + if (binary_format::fast_path_can_overflow() && + value > (std::numeric_limits::max)()) { + return false; + } + } + if (is_negative) { + value = -value; + } + return true; + } else { + // We do not have that fegetround() == FE_TONEAREST. + // Next is a modified Clinger's fast path, inspired by Jakub Jelínek's + // proposal + if (exponent >= 0 && + mantissa <= binary_format::max_mantissa_fast_path(exponent)) { +#if defined(__clang__) || defined(FASTFLOAT_32BIT) + // Clang may map 0 to -0.0 when fegetround() == FE_DOWNWARD + if (mantissa == 0) { + value = is_negative ? T(-0.) : T(0.); + return true; + } +#endif + value = T(mantissa) * binary_format::exact_power_of_ten(exponent); + if (is_negative) { + value = -value; + } + return true; + } + } + } + return false; +} + +/** + * This function overload takes parsed_number_string_t structure that is created + * and populated either by from_chars_advanced function taking chars range and + * parsing options or other parsing custom function implemented by user. + */ +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars_advanced(parsed_number_string_t &pns, T &value) noexcept { + static_assert(is_supported_float_type::value, + "only some floating-point types are supported"); + static_assert(is_supported_char_type::value, + "only char, wchar_t, char16_t and char32_t are supported"); + + from_chars_result_t answer; + + answer.ec = std::errc(); // be optimistic + answer.ptr = pns.lastmatch; + + if (!pns.too_many_digits && + clinger_fast_path_impl(pns.mantissa, pns.exponent, pns.negative, value)) + return answer; + + adjusted_mantissa am = + compute_float>(pns.exponent, pns.mantissa); + if (pns.too_many_digits && am.power2 >= 0) { + if (am != compute_float>(pns.exponent, pns.mantissa + 1)) { + am = compute_error>(pns.exponent, pns.mantissa); + } + } + // If we called compute_float>(pns.exponent, pns.mantissa) + // and we have an invalid power (am.power2 < 0), then we need to go the long + // way around again. This is very uncommon. + if (am.power2 < 0) { + am = digit_comp(pns, am); + } + to_float(pns.negative, am, value); + // Test for over/underflow. + if ((pns.mantissa != 0 && am.mantissa == 0 && am.power2 == 0) || + am.power2 == binary_format::infinite_power()) { + answer.ec = std::errc::result_out_of_range; + } + return answer; +} + +// Slow path: re-parse materializing the integer/fraction spans the hot no-span +// parse skipped, then run the full algorithm. The two callers reach it only +// through a fastfloat_unlikely branch, so the optimizer keeps this re-parse off +// the hot path on its own (no function-level noinline needed). +// from_chars_advanced already handles both the too_many_digits disambiguation +// and the am.power2<0 digit_comp recompute, so both slow branches collapse to +// one helper call. +template +FASTFLOAT_CONSTEXPR20 from_chars_result_t +parse_number_slow_path(UC const *first, UC const *last, T &value, + parse_options_t options, bool bjf) noexcept { + parsed_number_string_t pns = + bjf ? parse_number_string_impl(first, last, options, + true) + : parse_number_string_impl(first, last, options, + true); + return from_chars_advanced(pns, value); +} + +// Cold: the whole chars_format::javascript conversion. Kept out of line and out +// of from_chars_float_advanced so the common frame holds neither the javascript +// parser body nor a call that would force the parsed number string onto the +// stack. Mirrors the main path, only with the javascript parser. +template +fastfloat_never_inline FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars_float_javascript(UC const *first, UC const *last, T &value, + parse_options_t options) noexcept { + chars_format const fmt = detail::adjust_for_feature_macros(options.format); + if (uint64_t(fmt & chars_format::skip_white_space)) { + while ((first != last) && fast_float::is_space(*first)) { + first++; + } + } + from_chars_result_t answer; + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + if (first == last) { + return answer; + } + parsed_number_string_t pns = + parse_number_string_javascript(first, last, options, true); + if (!pns.valid) { + if (uint64_t(fmt & chars_format::no_infnan)) { + return answer; + } + return detail::parse_infnan(first, last, value, fmt); + } + return from_chars_advanced(pns, value); +} + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars_float_advanced(UC const *first, UC const *last, T &value, + parse_options_t options) noexcept { + + static_assert(is_supported_float_type::value, + "only some floating-point types are supported"); + static_assert(is_supported_char_type::value, + "only char, wchar_t, char16_t and char32_t are supported"); + + chars_format const fmt = detail::adjust_for_feature_macros(options.format); + + // Leave for the javascript conversion before anything else is live, so this + // compiles to a tail call and the common path keeps its registers. The cold + // function repeats the leading-whitespace and empty-input handling below. + if fastfloat_unlikely (uint64_t(fmt & detail::basic_javascript_fmt)) { + return from_chars_float_javascript(first, last, value, options); + } + + from_chars_result_t answer; + if (uint64_t(fmt & chars_format::skip_white_space)) { + while ((first != last) && fast_float::is_space(*first)) { + first++; + } + } + if (first == last) { + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + return answer; + } + bool const bjf = uint64_t(fmt & detail::basic_json_fmt) != 0; + + // Fast path: parse WITHOUT materializing the integer/fraction spans (read + // only by the rare slow paths). Skipping their stores keeps the fat + // parsed_number_string_t off the hot path. store_spans is a runtime argument, + // so this reuses the single parse_number_string_impl instantiation. + parsed_number_string_t pns = + bjf ? parse_number_string_impl(first, last, options, + false) + : parse_number_string_impl(first, last, options, + false); + if (!pns.valid) { + if (uint64_t(fmt & chars_format::no_infnan)) { + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + return answer; + } else { + return detail::parse_infnan(first, last, value, fmt); + } + } + + // Slow path A (rare): > 19 significant digits. The no-span parse left the + // mantissa un-truncated and skipped the span-based recompute; the cold helper + // re-parses with spans and runs the full algorithm. + // +// We have to disable -Wc++20-extensions for the [[unlikely]] attribute +// See comment for @jwakely at +// https://github.com/fastfloat/fast_float/pull/387#discussion_r3366943539 +// This is unfortunate. +#ifdef __clang__ +#pragma clang diagnostic push +#if (!defined(__APPLE_CC__) && __clang_major__ >= 10) || (__clang_major__ >= 13) +#pragma clang diagnostic ignored "-Wc++20-extensions" +#endif +#endif + if fastfloat_unlikely (pns.too_many_digits) { + return parse_number_slow_path(first, last, value, options, bjf); + } + answer.ec = std::errc(); // be optimistic + answer.ptr = pns.lastmatch; + + if (clinger_fast_path_impl(pns.mantissa, pns.exponent, pns.negative, value)) { + return answer; + } + + adjusted_mantissa am = + compute_float>(pns.exponent, pns.mantissa); + // Slow path B (rare): Eisel-Lemire could not resolve; digit_comp needs the + // integer/fraction spans. Route to the cold helper (clinger there is a + // dead-effect since it already failed here; the cold re-parse + digit_comp + // via from_chars_advanced reproduces this branch). + if fastfloat_unlikely (am.power2 < 0) { + return parse_number_slow_path(first, last, value, options, bjf); + } + to_float(pns.negative, am, value); + // Test for over/underflow. Marked unlikely so that clang keeps it as a + // branch instead of folding it into a chain of conditional moves that + // every conversion pays for. + if fastfloat_clang_unlikely ((pns.mantissa != 0 && am.mantissa == 0 && + am.power2 == 0) || + am.power2 == + binary_format::infinite_power()) { + answer.ec = std::errc::result_out_of_range; + } +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + return answer; +} + +template +FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars(UC const *first, UC const *last, T &value, int base) noexcept { + + static_assert(is_supported_integer_type::value, + "only integer types are supported"); + static_assert(is_supported_char_type::value, + "only char, wchar_t, char16_t and char32_t are supported"); + + parse_options_t options; + options.base = base; + return from_chars_advanced(first, last, value, options); +} + +template +FASTFLOAT_CONSTEXPR20 + typename std::enable_if::value, T>::type + integer_times_pow10(uint64_t mantissa, int decimal_exponent) noexcept { + T value; + if (clinger_fast_path_impl(mantissa, decimal_exponent, false, value)) + return value; + + adjusted_mantissa am = + compute_float>(decimal_exponent, mantissa); + to_float(false, am, value); + return value; +} + +template +FASTFLOAT_CONSTEXPR20 + typename std::enable_if::value, T>::type + integer_times_pow10(int64_t mantissa, int decimal_exponent) noexcept { + const bool is_negative = mantissa < 0; + const uint64_t m = static_cast(is_negative ? -mantissa : mantissa); + + T value; + if (clinger_fast_path_impl(m, decimal_exponent, is_negative, value)) + return value; + + adjusted_mantissa am = compute_float>(decimal_exponent, m); + to_float(is_negative, am, value); + return value; +} + +FASTFLOAT_CONSTEXPR20 inline double +integer_times_pow10(uint64_t mantissa, int decimal_exponent) noexcept { + return integer_times_pow10(mantissa, decimal_exponent); +} + +FASTFLOAT_CONSTEXPR20 inline double +integer_times_pow10(int64_t mantissa, int decimal_exponent) noexcept { + return integer_times_pow10(mantissa, decimal_exponent); +} + +// the following overloads are here to avoid surprising ambiguity for int, +// unsigned, etc. +template +FASTFLOAT_CONSTEXPR20 + typename std::enable_if::value && + std::is_integral::value && + !std::is_signed::value, + T>::type + integer_times_pow10(Int mantissa, int decimal_exponent) noexcept { + return integer_times_pow10(static_cast(mantissa), + decimal_exponent); +} + +template +FASTFLOAT_CONSTEXPR20 + typename std::enable_if::value && + std::is_integral::value && + std::is_signed::value, + T>::type + integer_times_pow10(Int mantissa, int decimal_exponent) noexcept { + return integer_times_pow10(static_cast(mantissa), + decimal_exponent); +} + +template +FASTFLOAT_CONSTEXPR20 typename std::enable_if< + std::is_integral::value && !std::is_signed::value, double>::type +integer_times_pow10(Int mantissa, int decimal_exponent) noexcept { + return integer_times_pow10(static_cast(mantissa), decimal_exponent); +} + +template +FASTFLOAT_CONSTEXPR20 typename std::enable_if< + std::is_integral::value && std::is_signed::value, double>::type +integer_times_pow10(Int mantissa, int decimal_exponent) noexcept { + return integer_times_pow10(static_cast(mantissa), decimal_exponent); +} + +template +FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars_int_advanced(UC const *first, UC const *last, T &value, + parse_options_t options) noexcept { + + static_assert(is_supported_integer_type::value, + "only integer types are supported"); + static_assert(is_supported_char_type::value, + "only char, wchar_t, char16_t and char32_t are supported"); + + chars_format const fmt = detail::adjust_for_feature_macros(options.format); + int const base = options.base; + + from_chars_result_t answer; + if (uint64_t(fmt & chars_format::skip_white_space)) { + while ((first != last) && fast_float::is_space(*first)) { + first++; + } + } + if (first == last || base < 2 || base > 36) { + answer.ec = std::errc::invalid_argument; + answer.ptr = first; + return answer; + } + + return parse_int_string(first, last, value, options); +} + +template struct from_chars_advanced_caller { + static_assert(TypeIx > 0, "unsupported type"); +}; + +template <> struct from_chars_advanced_caller<1> { + template + fastfloat_really_inline FASTFLOAT_CONSTEXPR20 static from_chars_result_t + call(UC const *first, UC const *last, T &value, + parse_options_t options) noexcept { + return from_chars_float_advanced(first, last, value, options); + } +}; + +template <> struct from_chars_advanced_caller<2> { + template + fastfloat_really_inline FASTFLOAT_CONSTEXPR20 static from_chars_result_t + call(UC const *first, UC const *last, T &value, + parse_options_t options) noexcept { + return from_chars_int_advanced(first, last, value, options); + } +}; + +template +fastfloat_really_inline FASTFLOAT_CONSTEXPR20 from_chars_result_t +from_chars_advanced(UC const *first, UC const *last, T &value, + parse_options_t options) noexcept { + return from_chars_advanced_caller< + size_t(is_supported_float_type::value) + + 2 * size_t(is_supported_integer_type::value)>::call(first, last, value, + options); +} + +} // namespace fast_float + +#endif diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/scripts/generate-fbreactnativespec.cjs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/scripts/generate-fbreactnativespec.cjs new file mode 100644 index 0000000000..c4545720f8 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/scripts/generate-fbreactnativespec.cjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const [reactNativeRoot, outputDir] = process.argv.slice(2); +if (!reactNativeRoot || !outputDir) { + throw new Error( + 'usage: generate-fbreactnativespec.cjs ', + ); +} +const reactNativeDir = path.resolve(reactNativeRoot); + +const packageRoot = path.resolve(__dirname, '../../..'); +const platformPackage = require(path.join(packageRoot, 'package.json')); +const expectedVersion = platformPackage.peerDependencies['react-native']; +const reactNativePackage = require(path.join(reactNativeDir, 'package.json')); +if (reactNativePackage.version !== expectedVersion) { + throw new Error( + `React Native ${reactNativePackage.version} is installed; this generated GPUI spec is pinned to ${expectedVersion}. Update the pin and verify the generated C++ before continuing.`, + ); +} + +const codegenUtils = require( + path.join(reactNativeDir, 'scripts/codegen/codegen-utils.js'), +); +const executor = require( + path.join(reactNativeDir, 'scripts/codegen/generate-specs-cli-executor.js'), +); +const schema = codegenUtils + .getCombineJSToSchema() + .combineSchemasInFileList([path.join(reactNativeDir, 'src')], 'android'); + +const generatedDir = path.resolve(outputDir); +fs.rmSync(generatedDir, { recursive: true, force: true }); +fs.mkdirSync(generatedDir, { recursive: true }); +executor.generateSpecFromInMemorySchema( + 'android', + schema, + generatedDir, + 'FBReactNativeSpec', + 'com.facebook.fbreact.specs', + 'all', + false, +); diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/src/lib.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/src/lib.rs new file mode 100644 index 0000000000..9f9bb58741 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/src/lib.rs @@ -0,0 +1,761 @@ +#[cxx::bridge(namespace = "rngpui")] +pub mod ffi { + // SAFETY: the signature matches the declaration in `GpuiImageLoader.h`. + unsafe extern "C++" { + include!("GpuiImageLoader.h"); + fn rn_gpui_on_image_load_result(request_id: u64, width: f64, height: f64, error: String); + } + + #[derive(Clone, Copy, Debug, Default, PartialEq)] + pub struct RectF { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + } + + #[derive(Clone, Copy, Debug, Default)] + pub struct EdgesF { + pub left: f32, + pub top: f32, + pub right: f32, + pub bottom: f32, + } + + #[derive(Clone, Copy, Debug, Default)] + pub struct CornersF { + pub tl: f32, + pub tr: f32, + pub br: f32, + pub bl: f32, + } + + #[derive(Clone, Debug, Default)] + pub struct ShadowF { + pub dx: f32, + pub dy: f32, + pub blur: f32, + pub spread: f32, + pub color: u32, + pub inset: bool, + } + + #[derive(Clone, Debug, Default)] + pub struct AccessibilityActionFlat { + pub name: String, + pub label: String, + } + + /// Colors are 0xAARRGGBB, 0 = none. + #[derive(Clone, Debug, Default)] + pub struct ViewPropsFlat { + pub opacity: f32, + pub background_color: u32, + pub border_widths: EdgesF, + pub content_insets: EdgesF, // RN layout border + padding insets + pub border_colors: [u32; 4], // left, top, right, bottom + pub border_styles: [u8; 4], // 0 solid, 1 dotted, 2 dashed + pub border_radii: CornersF, + pub outline_color: u32, + pub outline_width: f32, + pub outline_offset: f32, + pub outline_style: u8, + pub overflow_hidden: bool, + pub display_none: bool, // Fabric layout visibility, including retained descendants + pub pointer_events: u8, // 0 auto, 1 none, 2 box-none, 3 box-only + pub cursor: u8, // RN's `Cursor` ordinal; 0 = auto + pub shadows: Vec, + pub transform: [f32; 16], // column-major 4x4, resolved with layout + pub has_transform: bool, + pub test_id: String, + pub accessibility_label: String, + pub accessibility_hint: String, + pub accessibility_language: String, + pub accessibility_role: String, + pub accessibility_traits: u32, + pub accessibility_value_text: String, + pub accessibility_value_min: i32, + pub accessibility_value_max: i32, + pub accessibility_value_now: i32, + pub accessibility_has_value_min: bool, + pub accessibility_has_value_max: bool, + pub accessibility_has_value_now: bool, + pub accessibility_actions: Vec, + pub accessibility_labelled_by: Vec, + pub accessibility_order: Vec, + pub accessible: bool, + pub accessibility_disabled: bool, + pub accessibility_selected: bool, + pub accessibility_busy: bool, + pub accessibility_checked: u8, // 0 absent, 1 unchecked, 2 checked, 3 mixed + pub accessibility_expanded: i8, // -1 absent, 0 false, 1 true + pub accessibility_important: u8, // Auto, Yes, No, NoHideDescendants + pub accessibility_live_region: u8, // None, Polite, Assertive + pub accessibility_hidden: bool, + pub accessibility_modal: bool, + pub accessibility_responds_to_interaction: bool, + pub accessibility_has_action_listener: bool, + pub accessibility_has_tap_listener: bool, + pub accessibility_has_escape_listener: bool, + pub accessibility_has_magic_tap_listener: bool, + pub accessibility_native_id: String, + pub accessibility_ignores_invert_colors: bool, + pub accessibility_large_content_enabled: bool, + pub accessibility_large_content_title: String, + pub focusable: bool, + pub desktop_drag_region: bool, + /// "minimize" | "maximize" | "close" for a custom caption button. + pub on_context_menu: bool, + } + + #[derive(Clone, Debug, Default)] + pub struct TextFragmentFlat { + pub text: String, + pub font_family: String, + pub font_size: f32, + pub font_weight: u16, // 100..900 + pub italic: bool, + pub color: u32, + pub line_height: f32, // 0 = default + pub letter_spacing: f32, + pub underline: bool, + pub strikethrough: bool, + } + + #[derive(Clone, Debug, Default)] + pub struct ParagraphFlat { + pub fragments: Vec, + pub max_lines: i32, // 0 = unlimited + pub text_align: u8, // 0 natural, 1 left, 2 center, 3 right, 4 justify, 5 start, 6 end + pub ellipsize: u8, // 0 tail, 1 head, 2 middle, 3 clip + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + #[repr(u8)] + pub enum MountKind { + Create = 1, + Delete = 2, + Insert = 4, + Remove = 8, + Update = 16, + } + + /// One validated command argument. `kind` says which field holds it: 0 a + /// number (`number`), 1 a bool (`number` as 0 or 1), 2 a string (`text`), + /// 3 an array of numbers, 4 of bools (`numbers`, each 0 or 1), 5 of strings. + #[derive(Clone, Debug, Default)] + pub struct HostCommandArg { + pub kind: u8, + pub number: f64, + pub text: String, + pub numbers: Vec, + pub texts: Vec, + } + + #[derive(Clone, Debug)] + pub struct MountOp { + pub kind: MountKind, + pub tag: i32, + pub parent_tag: i32, // Insert/Remove/Update, else -1 + pub index: i32, // Insert/Remove, else -1 + pub component: String, // "View", "Paragraph", "Image", "ScrollView", ... + pub native_view_name: String, // Registered NativeViewFactory key + pub native_view_props: String, // JSON object supplied to the native view factory + pub frame: RectF, // layoutMetrics.frame, relative to parent, DIPs + pub props_changed: bool, + pub view: ViewPropsFlat, + pub extension_props_json: String, + /// The registry index of the host component this op is for, or `-1`. + pub host_index: i32, + /// An owned handle to the node's Fabric props (a `shared_ptr`), or zero. Set on `Create`, and on an `Update` only when the + /// props changed in a way the component can see: Rust takes ownership and + /// the generated accessors read the C++ fields in place. + pub host_props_handle: usize, + /// An owned handle to the node's event emitter, on `Create` only. + pub host_emitter_handle: usize, + /// The props that changed since the props Rust last received, one bit per + /// prop: the first 64 here, any more in `host_changed_more`. Computed in + /// C++ against `oldChildShadowView`, so it is relative to what the + /// component has seen even when several clones happened in between. + pub host_changed: u64, + pub host_changed_more: Vec, + pub has_paragraph: bool, + pub paragraph: ParagraphFlat, // filled for "Paragraph" from its state's AttributedString + pub layout_changed: bool, + } + + #[derive(Clone, Debug, Default)] + pub struct MountBatch { + pub surface_id: i32, + pub ops: Vec, + /// The size Fabric laid this surface's root out at, in DIPs. + pub content_width: f32, + pub content_height: f32, + /// Whether the surface's root was laid out at all in this commit. A + /// batch from a surface whose root has not been measured yet reports + /// zeros, which must not be mistaken for an empty tree. + pub has_content_size: bool, + } + + #[derive(Clone, Copy, Debug, Default)] + pub struct AnimatedPropsFlat { + pub tag: i32, + pub opacity: f32, + pub has_opacity: bool, + pub transform: [f32; 16], + pub has_transform: bool, + pub reset_transform: bool, + pub background_color: u32, + pub has_background_color: bool, + pub text_color: u32, + pub has_text_color: bool, + } + + #[derive(Clone, Debug)] + pub struct RustModuleMethod { + pub name: String, + pub arg_count: u32, + /// Arguments JS has to pass; the rest may be omitted. + pub required_args: u32, + /// 0 value, 1 void, 2 promise. + pub kind: u8, + /// Whether the spec returns `void`, which makes a promise resolve with + /// `undefined`. + pub returns_void: bool, + pub callback_args: Vec, + } + + #[derive(Clone, Debug)] + pub struct RustModuleEvent { + pub name: String, + pub has_value: bool, + } + + #[derive(Clone, Copy, Debug, Default)] + pub struct MeasureResult { + pub width: f32, + pub height: f32, + } + + extern "Rust" { + fn rn_gpui_load_image(uri: String, request_id: u64); + fn rn_gpui_image_is_cached(uri: &str) -> bool; + fn rn_gpui_load_font(family: &str, uri: &str) -> bool; + fn rn_gpui_render_text_image( + family: &str, + text: &str, + font_size: f64, + color: u32, + ) -> String; + /// Called on the JS/Fabric thread. Must not block, must not touch GPUI. + fn rn_gpui_on_mount(batch: MountBatch); + fn rn_gpui_on_animated_props(props: AnimatedPropsFlat); + fn rn_gpui_on_text_input_command(tag: i32, focus: bool); + fn rn_gpui_request_accessibility_focus(tag: i32); + fn rn_gpui_on_text_input_clear(tag: i32); + /// A command Fabric dispatched to a host component. The generated C++ + /// dispatcher already validated the arguments against the spec and + /// resolved the command to its index in the spec's `COMMANDS`. + fn rn_gpui_on_host_command(tag: i32, command: u32, args: Vec); + fn rn_gpui_set_animated_active(active: bool); + fn rn_gpui_dev_menu_show(); + fn rn_gpui_dev_setting(name: &str, enabled: bool); + /// Called from a module thread to run a task on the GPUI main thread. + /// The task stays in C++ under `id`; the main loop calls + /// `rn_gpui_run_main_task` for it. + fn rn_gpui_run_on_main(id: u64); + /// Whether the calling thread is the GPUI main thread. + fn rn_gpui_is_main_thread() -> bool; + /// Called on the JS/Fabric thread during layout. Thread-safe. + fn rn_gpui_measure_text( + p: &ParagraphFlat, + max_width: f32, + max_height: f32, + ) -> MeasureResult; + fn rn_gpui_log(level: i32, message: &str); + fn rn_gpui_on_surface_stopped(surface_id: i32); + } + + extern "Rust" { + /// Returns the handle of a registered module, or 0 when no module has + /// that name. + fn rn_gpui_rust_module_create(name: &str) -> u64; + fn rn_gpui_rust_module_methods(handle: u64) -> Vec; + fn rn_gpui_rust_module_events(handle: u64) -> Vec; + /// Runs a value method on the calling (JS) thread. The result is written + /// into `scope` and returned, or `kDynamicNoNode` when `ok` is false. + fn rn_gpui_rust_module_call_value( + handle: u64, + method: u32, + scope: u32, + args: u32, + ok: &mut bool, + code: &mut String, + message: &mut String, + ) -> u32; + /// Converts the arguments on the calling thread, then queues the call on + /// the module's thread; `ok` reports a conversion failure. + fn rn_gpui_rust_module_call_void( + handle: u64, + method: u32, + scope: u32, + args: u32, + ok: &mut bool, + code: &mut String, + message: &mut String, + ); + /// The same, settling the promise `request` identifies. + fn rn_gpui_rust_module_call_promise( + handle: u64, + method: u32, + scope: u32, + args: u32, + request: i32, + ); + /// Drops the module instance, which stops its thread. + fn rn_gpui_rust_module_drop(handle: u64); + } + + // SAFETY: the signatures match the declarations in `GpuiHostComponent.h`. + unsafe extern "C++" { + include!("GpuiHostComponent.h"); + + /// Releases a props handle a mount op gave Rust. + fn releaseHostPropsHandle(handle: usize); + /// Releases an event emitter handle a `Create` op gave Rust. + fn releaseHostEmitterHandle(handle: usize); + /// How many times the C++ registry was asked for a component since the + /// last reset. Only the test driver reads it. + fn hostEntryLookups() -> u64; + fn resetHostEntryLookups(); + /// Pushes a size a host component measured into its shadow node's state, + /// which is what Yoga reads, and asks for the commit that re-measures. + fn reportHostComponentSize(tag: i32, width: f32, height: f32); + /// Pushes the insets a container reported for its chrome into its shadow + /// node's state, where they become Yoga padding around the children. + fn reportHostComponentInsets(tag: i32, top: f32, right: f32, bottom: f32, left: f32); + } + + // SAFETY: the signatures match the declarations in `GpuiDynamic.h`. + unsafe extern "C++" { + include!("GpuiDynamic.h"); + + /// Values cross as nodes in a C++-owned arena, one scope per call, so + /// nothing is serialised to text. + fn dynamic_scope_open() -> u32; + fn dynamic_scope_close(scope: u32); + fn dynamic_node_new(scope: u32, kind: u8) -> u32; + fn dynamic_node_set_bool(scope: u32, node: u32, value: bool); + fn dynamic_node_set_number(scope: u32, node: u32, value: f64); + fn dynamic_node_set_text(scope: u32, node: u32, text: &str); + fn dynamic_node_set_buffer(scope: u32, node: u32, bytes: &[u8]); + fn dynamic_node_append(scope: u32, parent: u32, child: u32, key: &str); + fn dynamic_node_kind(scope: u32, node: u32) -> u8; + fn dynamic_node_bool(scope: u32, node: u32) -> bool; + fn dynamic_node_number(scope: u32, node: u32) -> f64; + fn dynamic_node_text(scope: u32, node: u32) -> String; + fn dynamic_node_buffer_len(scope: u32, node: u32) -> usize; + fn dynamic_node_buffer(scope: u32, node: u32) -> Vec; + fn dynamic_node_len(scope: u32, node: u32) -> usize; + fn dynamic_node_child(scope: u32, node: u32, index: usize) -> u32; + fn dynamic_node_key(scope: u32, node: u32, index: usize) -> String; + } + + // SAFETY: the signatures match the declarations in `GpuiRustTurboModules.h`. + unsafe extern "C++" { + include!("GpuiRustTurboModules.h"); + + fn rn_gpui_rust_module_result( + request: i32, + ok: bool, + scope: u32, + root: u32, + code: String, + message: String, + ); + fn rn_gpui_rust_callback_invoke(id: u64, scope: u32, root: u32); + fn rn_gpui_rust_callback_drop(id: u64); + fn rn_gpui_rust_module_emit( + handle: u64, + event: &str, + has_value: bool, + scope: u32, + root: u32, + ); + fn rn_gpui_rust_module_send_event(handle: u64, name: &str, scope: u32, root: u32); + /// Drops the promises and JS functions of the runtime that is going + /// away. + fn rn_gpui_rust_module_clear_pending(); + } + + // SAFETY: the signatures match the declarations in `GpuiHttpBridge.h`. + unsafe extern "C++" { + include!("GpuiHttpBridge.h"); + + /// Issue a request; the response arrives through `rn_gpui_http_complete`. + fn rn_gpui_http_request( + request_id: u64, + method: &str, + url: &str, + headers: &str, + body: &str, + ); + fn rn_gpui_http_request_cancel(request_id: u64); + } + + extern "Rust" { + /// Called by the C++ bridge when a request finishes. Implemented in + /// `rn-gpui`, which owns the future waiting for it. + fn rn_gpui_http_complete( + request_id: u64, + status: i32, + headers: &str, + // Bytes, not text: GPUI loads images and fonts through this client, + // and cxx rejects non-UTF-8 for `&str` by throwing. An image response + // crashed the app with "data for rust::Str is not utf-8". + body: &[u8], + error: &str, + ); + } + + // SAFETY: the signature matches the declaration in `GpuiStartupSignpost.h`. + unsafe extern "C++" { + include!("GpuiStartupSignpost.h"); + + /// A startup stage, as an `os_signpost` point event. A no-op off Apple. + fn rn_gpui_startup_signpost(stage: &str, elapsed_ms: u64); + } + + // SAFETY: the signatures match the declarations in `GpuiHost.h`. + unsafe extern "C++" { + include!("GpuiHost.h"); + type GpuiHost; + + fn new_gpui_host( + app_id: &str, + dev_server_host: &str, + dev_server_port: u32, + js_engine: &str, + dev_mode: bool, + ) -> UniquePtr; + fn load_script(self: Pin<&mut GpuiHost>, bundle_path: &str, source_url: &str) -> bool; + fn start_surface( + self: Pin<&mut GpuiHost>, + surface_id: i32, + module_name: &str, + width: f32, + height: f32, + scale: f32, + ); + fn start_surface_with_props( + self: Pin<&mut GpuiHost>, + surface_id: i32, + module_name: &str, + props_json: &str, + width: f32, + height: f32, + scale: f32, + ); + fn set_surface_size( + self: Pin<&mut GpuiHost>, + surface_id: i32, + width: f32, + height: f32, + scale: f32, + ); + /// Starts a surface whose size is a range rather than a point. Fabric + /// lays the root out against the constraints and reports the size it + /// used in every mount batch. + fn start_surface_with_constraints( + self: Pin<&mut GpuiHost>, + surface_id: i32, + module_name: &str, + props_json: &str, + min_width: f32, + min_height: f32, + max_width: f32, + max_height: f32, + scale: f32, + ); + /// Changes a running surface's constraints. + fn set_surface_constraints( + self: Pin<&mut GpuiHost>, + surface_id: i32, + min_width: f32, + min_height: f32, + max_width: f32, + max_height: f32, + scale: f32, + ); + fn set_window_top_inset(self: Pin<&mut GpuiHost>, top: f32); + fn set_window_metrics(self: Pin<&mut GpuiHost>, width: f32, height: f32, scale: f32); + fn set_system_appearance(self: Pin<&mut GpuiHost>, appearance: &str); + fn rn_gpui_run_main_task(id: u64); + fn stop_surface(self: Pin<&mut GpuiHost>, surface_id: i32); + fn request_reload(self: Pin<&mut GpuiHost>); + fn set_dev_server(self: Pin<&mut GpuiHost>, host: &str, port: u32) -> bool; + fn dev_server_address(self: &GpuiHost) -> String; + fn open_debugger(self: &GpuiHost); + fn js_frame_probe(self: &GpuiHost) -> u64; + fn js_heap_bytes(self: &GpuiHost) -> u64; + fn on_render(self: &GpuiHost) -> Result<()>; + fn on_frame(self: &GpuiHost, timestamp: f64) -> Result<()>; + fn on_pointer_frame(self: &GpuiHost, timestamp: f64) -> Result<()>; + /// payload_json is a JSON object string, e.g. {"pageX":10,"pageY":20} + fn dispatch_event(self: &GpuiHost, tag: i32, event_type: &str, payload_json: &str); + fn emit_device_event(self: &GpuiHost, event_name: &str); + fn emit_device_event_bool(self: &GpuiHost, event_name: &str, value: bool); + fn emit_device_event_json(self: &GpuiHost, event_name: &str, payload_json: &str); + fn dispatch_pointer_event( + self: &GpuiHost, + target_chain_json: &str, + x: f32, + y: f32, + phase: i32, + timestamp: f64, + ); + fn set_scroll_offset(self: &GpuiHost, tag: i32, x: f32, y: f32); + } +} + +mod rust_module; + +// The `extern "Rust"` declarations in the bridge resolve in this module, so the +// implementations are re-exported here. +pub use rust_module::{ + args_from_dynamic, rn_gpui_rust_module_call_promise, rn_gpui_rust_module_call_value, + rn_gpui_rust_module_call_void, rn_gpui_rust_module_create, rn_gpui_rust_module_drop, + rn_gpui_rust_module_events, rn_gpui_rust_module_methods, value_from_node, value_to_node, +}; + +use std::sync::OnceLock; + +pub use ffi::{ + CornersF, EdgesF, MeasureResult, MountBatch, MountKind, MountOp, ParagraphFlat, RectF, ShadowF, + TextFragmentFlat, ViewPropsFlat, +}; + +pub type MountHandler = Box; +pub type MeasureHandler = + Box ffi::MeasureResult + Send + Sync>; +pub type SurfaceStoppedHandler = Box; +pub type AnimatedPropsHandler = Box; +pub type AnimatedActiveHandler = Box; +pub type TextInputCommandHandler = Box; +pub type TextInputClearHandler = Box; +/// One command for one tag: the command's index and its validated arguments. +/// Called on the JS/Fabric thread. +pub type HostCommandHandler = Box) + Send + Sync>; +pub type AccessibilityFocusHandler = Box; +pub static MOUNT_HANDLER: OnceLock = OnceLock::new(); +pub static MEASURE_HANDLER: OnceLock = OnceLock::new(); +pub static SURFACE_STOPPED_HANDLER: OnceLock = OnceLock::new(); +pub static ANIMATED_PROPS_HANDLER: OnceLock = OnceLock::new(); +pub static ANIMATED_ACTIVE_HANDLER: OnceLock = OnceLock::new(); +pub static TEXT_INPUT_COMMAND_HANDLER: OnceLock = OnceLock::new(); +pub static TEXT_INPUT_CLEAR_HANDLER: OnceLock = OnceLock::new(); +pub static HOST_COMMAND_HANDLER: OnceLock = OnceLock::new(); +pub static ACCESSIBILITY_FOCUS_HANDLER: OnceLock = OnceLock::new(); +pub type MainTaskHandler = Box; +pub static MAIN_TASK_HANDLER: OnceLock = OnceLock::new(); +pub static MAIN_THREAD_ID: OnceLock = OnceLock::new(); + +/// Records the calling thread as the GPUI main thread. The main loop calls this +/// before it can receive main-thread tasks. +pub fn note_main_thread() { + let _ = MAIN_THREAD_ID.set(std::thread::current().id()); +} +pub type DevMenuShowHandler = Box; +pub static DEV_MENU_SHOW_HANDLER: OnceLock = OnceLock::new(); +pub type DevSettingHandler = Box; +pub static DEV_SETTING_HANDLER: OnceLock = OnceLock::new(); +pub type DevMenuPerformHandler = Box; +pub static DEV_MENU_PERFORM_HANDLER: OnceLock = OnceLock::new(); +pub type TrayClickHandler = Box; +pub static TRAY_CLICK_HANDLER: OnceLock = OnceLock::new(); +pub type TrayMenuPerformHandler = Box; +pub static TRAY_MENU_PERFORM_HANDLER: OnceLock = OnceLock::new(); +pub type MenuBarPerformHandler = Box; +pub static MENU_BAR_PERFORM_HANDLER: OnceLock = OnceLock::new(); +/// A finished HTTP request, reported by the bridge. +pub type HttpCompleteHandler = Box, String) + Send + Sync>; +pub static HTTP_COMPLETE_HANDLER: OnceLock = OnceLock::new(); + +pub type ImageHandler = Box; +pub static IMAGE_HANDLER: OnceLock = OnceLock::new(); +pub type ImageCacheQueryHandler = Box bool + Send + Sync>; +pub static IMAGE_CACHE_QUERY_HANDLER: OnceLock = OnceLock::new(); +pub type FontLoadHandler = Box bool + Send + Sync>; +pub type TextImageHandler = Box String + Send + Sync>; +pub static FONT_LOAD_HANDLER: OnceLock = OnceLock::new(); +pub static TEXT_IMAGE_HANDLER: OnceLock = OnceLock::new(); + +fn rn_gpui_load_font(family: &str, uri: &str) -> bool { + FONT_LOAD_HANDLER + .get() + .is_some_and(|handler| handler(family, uri)) +} + +fn rn_gpui_render_text_image(family: &str, text: &str, font_size: f64, color: u32) -> String { + TEXT_IMAGE_HANDLER + .get() + .map_or_else(String::new, |handler| { + handler(family, text, font_size, color) + }) +} + +/// The C++ bridge reports a finished request here. +fn rn_gpui_http_complete(request_id: u64, status: i32, headers: &str, body: &[u8], error: &str) { + if let Some(handler) = HTTP_COMPLETE_HANDLER.get() { + handler( + request_id, + status, + headers.to_string(), + body.to_vec(), + error.to_string(), + ); + } +} + +fn rn_gpui_load_image(uri: String, request_id: u64) { + if let Some(handler) = IMAGE_HANDLER.get() { + handler(uri, request_id); + } else { + ffi::rn_gpui_on_image_load_result( + request_id, + 0.0, + 0.0, + "image loader is not initialized".into(), + ); + } +} + +fn rn_gpui_image_is_cached(uri: &str) -> bool { + IMAGE_CACHE_QUERY_HANDLER + .get() + .map(|handler| handler(uri)) + .unwrap_or(false) +} + +pub const ALL_SURFACES: i32 = -1; + +/// Fabric emits no Delete mutations for a dying surface, so this is the only +/// cleanup signal. +fn rn_gpui_on_surface_stopped(surface_id: i32) { + match SURFACE_STOPPED_HANDLER.get() { + Some(handler) => handler(surface_id), + None => log::debug!( + "rn_gpui_on_surface_stopped({surface_id}): no handler registered yet — dropped" + ), + } +} + +/// Called on the Fabric thread. Queues the batch for the GPUI main thread and +/// returns at once, so mounting never blocks on rendering. +fn rn_gpui_on_mount(batch: ffi::MountBatch) { + match MOUNT_HANDLER.get() { + Some(handler) => handler(batch), + None => { + log::debug!( + "rn_gpui_on_mount: dropping batch for surface {} ({} ops) — no handler registered", + batch.surface_id, + batch.ops.len() + ); + } + } +} + +fn rn_gpui_on_animated_props(props: ffi::AnimatedPropsFlat) { + if let Some(handler) = ANIMATED_PROPS_HANDLER.get() { + handler(props); + } +} + +fn rn_gpui_set_animated_active(active: bool) { + if let Some(handler) = ANIMATED_ACTIVE_HANDLER.get() { + handler(active); + } +} + +fn rn_gpui_on_text_input_command(tag: i32, focus: bool) { + if let Some(handler) = TEXT_INPUT_COMMAND_HANDLER.get() { + handler(tag, focus); + } +} + +fn rn_gpui_on_text_input_clear(tag: i32) { + if let Some(handler) = TEXT_INPUT_CLEAR_HANDLER.get() { + handler(tag); + } +} + +fn rn_gpui_on_host_command(tag: i32, command: u32, args: Vec) { + if let Some(handler) = HOST_COMMAND_HANDLER.get() { + handler(tag, command, args); + } +} + +fn rn_gpui_request_accessibility_focus(tag: i32) { + if let Some(handler) = ACCESSIBILITY_FOCUS_HANDLER.get() { + handler(tag); + } +} + +fn rn_gpui_dev_menu_show() { + if let Some(handler) = DEV_MENU_SHOW_HANDLER.get() { + handler(); + } +} + +fn rn_gpui_run_on_main(id: u64) { + match MAIN_TASK_HANDLER.get() { + Some(handler) => handler(id), + None => { + // No main loop is listening (startup, or a build without the + // desktop module), so the task would never run. Say so once. + static WARNED: std::sync::Once = std::sync::Once::new(); + WARNED.call_once(|| { + log::warn!("rn_gpui_run_on_main: no main-thread handler installed; task dropped"); + }); + } + } +} + +fn rn_gpui_is_main_thread() -> bool { + MAIN_THREAD_ID + .get() + .is_some_and(|id| *id == std::thread::current().id()) +} + +fn rn_gpui_dev_setting(name: &str, enabled: bool) { + if let Some(handler) = DEV_SETTING_HANDLER.get() { + handler(name.to_string(), enabled); + } +} + +fn rn_gpui_measure_text( + p: &ffi::ParagraphFlat, + max_width: f32, + max_height: f32, +) -> ffi::MeasureResult { + match MEASURE_HANDLER.get() { + Some(handler) => handler(p, max_width, max_height), + None => ffi::MeasureResult::default(), + } +} + +fn rn_gpui_log(level: i32, message: &str) { + match level { + 0 => log::debug!("{message}"), + 1 => log::info!("{message}"), + 2 => log::warn!("{message}"), + _ => log::error!("{message}"), + } +} + +// SAFETY: the C++ GpuiHost is created, driven and dropped from the GPUI main +// thread; the wrapper is moved to Rust, so cxx needs this declared explicitly. +unsafe impl Send for ffi::GpuiHost {} +// SAFETY: as for `Send`, every call goes through the GPUI main thread. +unsafe impl Sync for ffi::GpuiHost {} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/src/rust_module.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/src/rust_module.rs new file mode 100644 index 0000000000..265a1552ed --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui-sys/src/rust_module.rs @@ -0,0 +1,481 @@ +//! The Rust side of the TurboModule bridge: the handle registry, the value +//! conversions, and the hooks the module crate calls back through. + +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; + +use rn_gpui_module::{ + descriptor, EventEmitter, HostHooks, ModuleDescriptor, ModuleInstance, ModuleQueue, + ModuleThread, RawCallback, Value, +}; + +use crate::ffi; + +const DYNAMIC_NULL: u8 = 0; +const DYNAMIC_BOOL: u8 = 1; +const DYNAMIC_NUMBER: u8 = 2; +const DYNAMIC_STRING: u8 = 3; +const DYNAMIC_ARRAY: u8 = 4; +const DYNAMIC_OBJECT: u8 = 5; +const DYNAMIC_BUFFER: u8 = 6; +const NO_NODE: u32 = 0xffff_ffff; + +/// A module instance JS asked for, with the queue its `void` and `Promise` +/// methods run on. +struct Handle { + instance: Arc, + descriptor: &'static ModuleDescriptor, + queue: OnceLock, +} + +static HANDLES: OnceLock>>> = OnceLock::new(); +static NEXT_HANDLE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); + +fn handles() -> &'static Mutex>> { + HANDLES.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn handle_for(id: u64) -> Option> { + handles() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .get(&id) + .cloned() +} + +impl Handle { + fn queue(&self) -> &ModuleQueue { + self.queue + .get_or_init(|| ModuleQueue::new(self.descriptor.name)) + } +} + +/// Converts the JS arguments of a call; the positions in `callback_args` hold a +/// `RawCallback` id the C++ side put there. +pub fn args_from_dynamic(scope: u32, args: u32, callback_args: &[usize]) -> Vec { + let converted = value_from_node(scope, args); + let Value::Array(items) = converted else { + return vec![converted]; + }; + items + .into_iter() + .enumerate() + .map(|(index, item)| { + if callback_args.contains(&index) { + match item { + Value::Number(id) => Value::Callback(RawCallback::new(id as u64)), + other => other, + } + } else { + item + } + }) + .collect() +} + +pub fn value_from_node(scope: u32, node: u32) -> Value { + match ffi::dynamic_node_kind(scope, node) { + DYNAMIC_BOOL => Value::Bool(ffi::dynamic_node_bool(scope, node)), + DYNAMIC_NUMBER => Value::Number(ffi::dynamic_node_number(scope, node)), + DYNAMIC_STRING => Value::String(ffi::dynamic_node_text(scope, node)), + DYNAMIC_BUFFER => Value::Buffer(ffi::dynamic_node_buffer(scope, node)), + DYNAMIC_ARRAY => Value::Array( + (0..ffi::dynamic_node_len(scope, node)) + .map(|index| value_from_node(scope, ffi::dynamic_node_child(scope, node, index))) + .collect(), + ), + DYNAMIC_OBJECT => Value::Object( + (0..ffi::dynamic_node_len(scope, node)) + .map(|index| { + let key = ffi::dynamic_node_key(scope, node, index); + let child = ffi::dynamic_node_child(scope, node, index); + (key, value_from_node(scope, child)) + }) + .collect(), + ), + _ => Value::Null, + } +} + +/// Writes the value tree into a scope and returns its root node. A callback +/// never appears in a result, so it becomes null instead of failing the call. +pub fn value_to_node(scope: u32, value: Value) -> u32 { + let (kind, number, text) = match &value { + Value::Null | Value::Callback(_) => (DYNAMIC_NULL, 0.0, String::new()), + Value::Bool(flag) => (DYNAMIC_BOOL, if *flag { 1.0 } else { 0.0 }, String::new()), + Value::Number(number) => (DYNAMIC_NUMBER, *number, String::new()), + Value::String(text) => (DYNAMIC_STRING, 0.0, text.clone()), + Value::Buffer(_) => (DYNAMIC_BUFFER, 0.0, String::new()), + Value::Array(_) => (DYNAMIC_ARRAY, 0.0, String::new()), + Value::Object(_) => (DYNAMIC_OBJECT, 0.0, String::new()), + }; + let node = ffi::dynamic_node_new(scope, kind); + match kind { + DYNAMIC_BOOL => ffi::dynamic_node_set_bool(scope, node, number != 0.0), + DYNAMIC_NUMBER => ffi::dynamic_node_set_number(scope, node, number), + DYNAMIC_STRING => ffi::dynamic_node_set_text(scope, node, &text), + DYNAMIC_BUFFER => { + if let Value::Buffer(bytes) = value { + ffi::dynamic_node_set_buffer(scope, node, &bytes); + } + } + DYNAMIC_ARRAY | DYNAMIC_OBJECT => { + let children = match value { + Value::Array(items) => items + .into_iter() + .map(|item| (String::new(), item)) + .collect::>(), + Value::Object(fields) => fields, + _ => Vec::new(), + }; + for (key, item) in children { + let child = value_to_node(scope, item); + ffi::dynamic_node_append(scope, node, child, &key); + } + } + _ => {} + } + node +} + +/// Builds a value in a fresh scope for a result that crosses later. The caller +/// closes the scope. +fn scope_for_value(value: Value) -> (u32, u32) { + let scope = ffi::dynamic_scope_open(); + let root = value_to_node(scope, value); + (scope, root) +} + +fn result_parts(error: rn_gpui_module::ModuleError) -> (bool, String, String) { + (false, error.code.unwrap_or_default(), error.message) +} + +fn report_result(request: i32, result: Result) { + let (ok, value, code, message) = match result { + Ok(value) => (true, Some(value), String::new(), String::new()), + Err(error) => { + let (_, code, message) = result_parts(error); + (false, None, code, message) + } + }; + let (scope, root) = match value { + Some(value) => scope_for_value(value), + None => (ffi::dynamic_scope_open(), NO_NODE), + }; + ffi::rn_gpui_rust_module_result(request, ok, scope, root, code, message); + ffi::dynamic_scope_close(scope); +} + +fn install_hooks() { + static INSTALLED: OnceLock<()> = OnceLock::new(); + INSTALLED.get_or_init(|| { + rn_gpui_module::set_host_hooks(HostHooks { + invoke_callback: |id, args| { + let (scope, root) = scope_for_value(Value::Array(args)); + ffi::rn_gpui_rust_callback_invoke(id, scope, root); + ffi::dynamic_scope_close(scope); + }, + drop_callback: |id| ffi::rn_gpui_rust_callback_drop(id), + emit_event: |module, event, value| { + let has_value = value.is_some(); + let (scope, root) = match value { + Some(value) => scope_for_value(value), + None => (ffi::dynamic_scope_open(), NO_NODE), + }; + ffi::rn_gpui_rust_module_emit(module, event, has_value, scope, root); + ffi::dynamic_scope_close(scope); + }, + send_event: |module, name, body| { + let (scope, root) = scope_for_value(body); + ffi::rn_gpui_rust_module_send_event(module, name, scope, root); + ffi::dynamic_scope_close(scope); + }, + log: |message| log::error!("{message}"), + }); + }); +} + +pub fn rn_gpui_rust_module_create(name: &str) -> u64 { + let Some(descriptor) = descriptor(name) else { + return 0; + }; + install_hooks(); + let id = NEXT_HANDLE.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + let instance = (descriptor.create)(EventEmitter::new(id)); + handles() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert( + id, + Arc::new(Handle { + instance, + descriptor, + queue: OnceLock::new(), + }), + ); + id +} + +pub fn rn_gpui_rust_module_methods(handle: u64) -> Vec { + let Some(handle) = handle_for(handle) else { + return Vec::new(); + }; + handle + .descriptor + .methods + .iter() + .map(|method| ffi::RustModuleMethod { + name: method.name.to_string(), + arg_count: method.arg_count as u32, + required_args: method.required_args as u32, + kind: match method.kind { + rn_gpui_module::MethodKind::Value => 0, + rn_gpui_module::MethodKind::Void => 1, + rn_gpui_module::MethodKind::Promise => 2, + }, + returns_void: method.returns_void, + callback_args: method + .callback_args + .iter() + .map(|index| *index as u32) + .collect(), + }) + .collect() +} + +pub fn rn_gpui_rust_module_events(handle: u64) -> Vec { + let Some(handle) = handle_for(handle) else { + return Vec::new(); + }; + handle + .descriptor + .events + .iter() + .map(|event| ffi::RustModuleEvent { + name: event.name.to_string(), + has_value: event.has_value, + }) + .collect() +} + +pub fn rn_gpui_rust_module_call_value( + handle: u64, + method: u32, + scope: u32, + args: u32, + ok: &mut bool, + code: &mut String, + message: &mut String, +) -> u32 { + let Some(handle) = handle_for(handle) else { + *ok = false; + *message = "the Rust module is gone".to_string(); + return NO_NODE; + }; + let Some(info) = handle.descriptor.methods.get(method as usize) else { + *ok = false; + *message = "unknown method".to_string(); + return NO_NODE; + }; + let converted = args_from_dynamic(scope, args, info.callback_args); + match handle.instance.call_value(method as usize, converted) { + Ok(value) => { + *ok = true; + value_to_node(scope, value) + } + Err(error) => { + let (_, error_code, error_message) = result_parts(error); + *ok = false; + *code = error_code; + *message = error_message; + NO_NODE + } + } +} + +pub fn rn_gpui_rust_module_call_void( + handle: u64, + method: u32, + scope: u32, + args: u32, + ok: &mut bool, + code: &mut String, + message: &mut String, +) { + let Some(handle) = handle_for(handle) else { + *ok = false; + *message = "the Rust module is gone".to_string(); + return; + }; + let Some(info) = handle.descriptor.methods.get(method as usize) else { + *ok = false; + *message = "unknown method".to_string(); + return; + }; + // Arguments are converted here, on the calling thread, so a bad argument is + // a thrown JS error rather than a silent failure on the module thread. + let converted = args_from_dynamic(scope, args, info.callback_args); + match handle + .instance + .clone() + .prepare_void(method as usize, converted) + { + Ok(work) => { + *ok = true; + post( + &handle, + Box::new(move || { + work(); + Box::pin(async {}) + }), + ); + } + Err(error) => { + let (_, error_code, error_message) = result_parts(error); + *ok = false; + *code = error_code; + *message = error_message; + } + } +} + +pub fn rn_gpui_rust_module_call_promise( + handle: u64, + method: u32, + scope: u32, + args: u32, + request: i32, +) { + let Some(module) = handle_for(handle) else { + report_result( + request, + Err(rn_gpui_module::ModuleError::new("the Rust module is gone")), + ); + return; + }; + let Some(info) = module.descriptor.methods.get(method as usize) else { + report_result( + request, + Err(rn_gpui_module::ModuleError::new("unknown method")), + ); + return; + }; + let converted = args_from_dynamic(scope, args, info.callback_args); + // The future is created on the module's thread: it does not have to be + // `Send`, only the job that builds it does. + let instance = module.instance.clone(); + let job: rn_gpui_module::Job = Box::new(move || { + let future = instance.call_promise(method as usize, converted); + Box::pin(async move { + // A panic here would kill the module thread, and the host is the + // only side that can settle the promise. + let reported = rn_gpui_module::__private::catch_async(async move { + match future { + Ok(future) => report_result(request, future.await), + Err(error) => report_result(request, Err(error)), + } + }) + .await; + if let Err(error) = reported { + rn_gpui_module::log_error("a Rust promise result", &error); + } + }) + }); + post(&module, job); +} + +pub fn rn_gpui_rust_module_drop(handle_id: u64) { + // Removing the last reference stops the queue, which ends its thread and + // drops unfinished futures; results still in flight are discarded by C++ + // because their request ids are gone. + handles() + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .remove(&handle_id); +} + +fn post(handle: &Handle, job: rn_gpui_module::Job) { + match handle.descriptor.thread { + ModuleThread::Own => handle.queue().post(job), + ModuleThread::Main => rn_gpui_module::spawn_main(job), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_nested_value_round_trips_through_a_scope() { + let value = Value::Object(vec![ + ("text".into(), Value::String("hello".into())), + ("count".into(), Value::Number(2.5)), + ("whole".into(), Value::Number(3.0)), + ("flag".into(), Value::Bool(true)), + ("nothing".into(), Value::Null), + ( + "items".into(), + Value::Array(vec![ + Value::Number(1.0), + Value::Object(vec![("deep".into(), Value::String("yes".into()))]), + ]), + ), + ]); + let scope = ffi::dynamic_scope_open(); + let root = value_to_node(scope, value); + let round_tripped = value_from_node(scope, root); + ffi::dynamic_scope_close(scope); + + let Value::Object(fields) = &round_tripped else { + panic!("expected an object, got {round_tripped:?}"); + }; + let field = |name: &str| { + fields + .iter() + .find(|(key, _)| key == name) + .map(|(_, value)| value) + .unwrap_or_else(|| panic!("missing field {name}")) + }; + assert!(matches!(field("text"), Value::String(text) if text == "hello")); + assert!(matches!(field("count"), Value::Number(number) if *number == 2.5)); + assert!(matches!(field("whole"), Value::Number(number) if *number == 3.0)); + assert!(matches!(field("flag"), Value::Bool(true))); + assert!(matches!(field("nothing"), Value::Null)); + let Value::Array(items) = field("items") else { + panic!("expected an array"); + }; + assert_eq!(items.len(), 2); + assert!(matches!(&items[0], Value::Number(number) if *number == 1.0)); + let Value::Object(nested) = &items[1] else { + panic!("expected a nested object"); + }; + assert!(matches!(&nested[0].1, Value::String(text) if text == "yes")); + } + + #[test] + fn callback_positions_become_raw_callbacks() { + let scope = ffi::dynamic_scope_open(); + let args = value_to_node( + scope, + Value::Array(vec![Value::String("key".into()), Value::Number(41.0)]), + ); + let converted = args_from_dynamic(scope, args, &[1]); + ffi::dynamic_scope_close(scope); + assert!(matches!(&converted[0], Value::String(text) if text == "key")); + match &converted[1] { + Value::Callback(callback) => assert_eq!(callback.id(), 41), + other => panic!("expected a callback, got {other:?}"), + } + } +} + +/// `GpuiHost.cpp` calls this hook, which the `rn-gpui` crate defines. This +/// crate's own unit-test binary links the C++ core without `rn-gpui`, so on +/// Linux the reference is unresolved. Windows uses an `/alternatename` fallback +/// in the C++ instead. Lives here rather than in `lib.rs` because `lib.rs` is a +/// prebuilt input and this is test-only. +#[cfg(test)] +#[no_mangle] +pub extern "C" fn rn_gpui_accessibility_active() -> bool { + false +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/Cargo.toml b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/Cargo.toml new file mode 100644 index 0000000000..9a67d93438 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/Cargo.toml @@ -0,0 +1,99 @@ +[package] +name = "react-native-gpui" +version = "0.1.0" +edition = "2021" +links = "rn_gpui_runtime" + +[features] +# Test/probe controls: synthetic input, resize/scale/headless overrides, custom +# window-state paths, startup tracing, native-view probes, and frame capture. +# `RN_GPUI_SCREENSHOT` renders the window's own scene to a PNG off-screen. +# gpui's `Window::render_to_image` is gated on `test-support` in gpui, +# gpui_macos, gpui_apple and (with the Linux patches) gpui_linux, so the +# feature has to be enabled at the platform crate too. +visual-capture = ["gpui_platform/test-support"] +# A local-only WebSocket test driver. It is absent from normal builds. +test-driver = ["visual-capture", "dep:tungstenite"] +# Enables Window's draw/present histograms for the 10k-node benchmark. +perf-timing = ["gpui/profiler"] + +[build-dependencies] +rn-gpui-module-build = { path = "../rn-gpui-module-build" } + +[dependencies] +rn-gpui-sys = { path = "../rn-gpui-sys" } +rn-gpui-module = { path = "../rn-gpui-module" } +gpui = { workspace = true } +cxx = { workspace = true } +async-channel = { workspace = true } +anyhow = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } +serde_json = { workspace = true } +futures = { workspace = true } +http_client = { workspace = true } +fontdue = { workspace = true } +image = { version = "0.25.1" } +base64 = "0.22" +# Menu-bar (tray) apps: NSStatusItem on macOS, the notification area +# on Windows, StatusNotifierItem on Linux. `default-features = false` because +# the crate default is its GTK Linux backend; the Linux block below selects the +# GTK-free one instead (Cargo unifies features per crate, so a default here +# would keep the GTK backend enabled no matter what the target block asked for). +# macOS and Windows backends are not feature-gated, so they are unaffected. +tray-icon = { version = "0.26", default-features = false } +tungstenite = { version = "0.26", default-features = false, features = ["handshake"], optional = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("cargo-clippy"))'] } + +[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] +# Native context menus. On macOS this is an NSMenu shown for the +# window's NSView; on Windows a Win32 popup menu on the HWND. +muda = "0.15" + +[target.'cfg(target_os = "macos")'.dependencies] +# `font-kit` makes gpui_macos build a real (CoreText) text system; without it +# every glyph paints as nothing. +gpui_platform = { workspace = true, features = ["font-kit"] } +cocoa = "0.25" +objc = "0.2" +raw-window-handle = "0.6" + +[target.'cfg(target_os = "linux")'.dependencies] +# gpui_platform intentionally has no default features. Enable both backends so +# Linux users can run under either X11 or Wayland. +gpui_platform = { workspace = true, features = ["x11", "wayland"] } +# `tray-icon`'s default Linux backend is `libappindicator`, which is a GTK +# widget: `app_indicator_new` needs a GTK screen and its menu needs the GTK main +# loop. GPUI runs its own X11 loop and never enters a GTK one, so the icon was +# created but never appeared (the process logged +# `gtk_icon_theme_get_for_screen: assertion 'GDK_IS_SCREEN (screen)' failed`). +# `ksni` is the same StatusNotifierItem protocol over plain D-Bus, served from a +# worker thread, and needs no GTK. +tray-icon = { version = "0.26", default-features = false, features = ["ksni"] } +# Already in the tree through `ksni`; named directly so the tray can ask the +# session bus whether a StatusNotifier host is running (see +# `tray_host_available`). "blocking-api" supplies the synchronous `Connection`. +zbus = { version = "5", default-features = false, features = ["blocking-api"] } + +# X11 window calls GPUI does not expose. Wayland has no +# equivalent, so those sessions stay `unsupported`. The X11 path reads the +# window's raw handle, which gpui exposes through raw-window-handle. +x11-dl = "2.21" +raw-window-handle = "0.6" + +[target.'cfg(target_os = "windows")'.dependencies] +gpui_platform = { workspace = true } +# Win32 window calls GPUI does not expose. +windows-sys = { version = "0.59", features = [ + "Win32_Foundation", + # `MonitorFromPoint` and `GetMonitorInfoW`: the work area a popover is placed in. + "Win32_Graphics_Gdi", + # `GetDpiForSystem`: the scale a tray icon's physical rectangle is divided by. + "Win32_UI_HiDpi", + "Win32_UI_WindowsAndMessaging", +] } +# The HWND for the native menu bar and context menus, read from the window's +# raw handle the same way macOS reads its NSView. +raw-window-handle = "0.6" diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/build.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/build.rs new file mode 100644 index 0000000000..a4b1a7e243 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/build.rs @@ -0,0 +1,69 @@ +//! rpath metadata for the host executable and consuming desktop app. +//! +//! Linked libraries built with `@rpath/...` install names — `libjsi.dylib` from +//! our CMake build, and any shared library an installed JS engine package adds — +//! so an executable that links them needs those directories on its rpath at run +//! time. +//! The sys crate exports paths as `DEP_RN_GPUI_CORE_RPATHS`. Re-export them as +//! `DEP_RN_GPUI_RUNTIME_RPATHS` for the app-local desktop crate's build.rs; a +//! downstream executable must carry its own rpath because the host runtime is +//! an rlib dependency. +//! +//! The same file re-exports the engine's Node-API export list, which the app's +//! build script turns into a module definition (see `DEP_RN_GPUI_CORE_NODE_API_EXPORTS`). + +fn main() { + rn_gpui_module_build::generate(&[ + "../../src-gpui/src/private/specs/NativeGpuiDesktop.ts", + "../../src-gpui/src/private/specs/NativeGpuiAlertManager.ts", + "../../src-gpui/src/private/specs/NativeGpuiClipboard.ts", + "../../src-gpui/src/private/specs/NativeGpuiLinkingManager.ts", + "../../src-gpui/src/private/specs/NativeGpuiPerfMonitor.ts", + "../../src-gpui/src/private/specs/NativeGpuiTray.ts", + "../../src-gpui/src/private/specs/NativeGpuiWindows.ts", + "../../src-gpui/src/private/specs/NativeGpuiAppMenu.ts", + "../../src-gpui/src/private/specs/NativeGpuiContextMenu.ts", + "../../src-gpui/src/private/specs/NativeGpuiDevMenu.ts", + "../../src-gpui/src/private/specs/NativeGpuiShortcuts.ts", + ]); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-env-changed=DEP_RN_GPUI_CORE_RPATHS"); + println!("cargo:rerun-if-env-changed=DEP_RN_GPUI_CORE_NODE_API_EXPORTS"); + println!("cargo:rerun-if-env-changed=DEP_RN_GPUI_CORE_NODE_API_LIBRARY"); + + // Windows needs the Node-API forwarding even though it has no rpath, so + // re-export those two before the early return below. + for (from, to) in [ + ( + "DEP_RN_GPUI_CORE_NODE_API_EXPORTS", + "cargo:node_api_exports", + ), + ( + "DEP_RN_GPUI_CORE_NODE_API_LIBRARY", + "cargo:node_api_library", + ), + ] { + if let Ok(value) = std::env::var(from) { + if !value.is_empty() { + println!("{to}={value}"); + } + } + } + + let Ok(dirs) = std::env::var("DEP_RN_GPUI_CORE_RPATHS") else { + return; + }; + let dirs = dirs + .split('|') + .filter(|dir| !dir.is_empty()) + .collect::>(); + println!("cargo:rpaths={}", dirs.join("|")); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + // Windows locates DLLs beside the executable or through PATH; ELF and + // Mach-O rpath linker flags are invalid for MSVC and MinGW targets. + return; + } + for dir in dirs { + println!("cargo:rustc-link-arg=-Wl,-rpath,{dir}"); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/accessibility.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/accessibility.rs new file mode 100644 index 0000000000..b56f7a8721 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/accessibility.rs @@ -0,0 +1,159 @@ +use crate::{node_view::RNNodeView, store::StoreGlobal}; +use gpui::{App, Entity, FocusHandle, Global, Window}; +use std::collections::HashSet; + +#[derive(Default)] +struct ModalFocusState { + active: Option, + visible: HashSet, + restore: Option<(i32, FocusHandle)>, + pending_focus: bool, +} +impl Global for ModalFocusState {} + +pub(crate) fn hidden_by_active_modal(tag: i32, cx: &App) -> bool { + cx.try_global::() + .is_some_and(|state| state.active.is_some() && !state.visible.contains(&tag)) +} + +fn collect_modal( + node: &Entity, + cx: &App, + tags: &mut HashSet, + focus: &mut Vec, +) { + let node = node.read(cx); + if node.view.display_none + || node.view.accessibility_hidden + || node.view.accessibility_important == 3 + { + return; + } + tags.insert(node.tag); + if node.focusable + && !node.view.accessibility_disabled + && node.view.accessibility_responds_to_interaction + { + if let Some(handle) = node.accessibility_focus_handle() { + focus.push(handle); + } + } + for (_, child) in &node.children { + collect_modal(child, cx, tags, focus); + } +} + +/// Called after the mount/render transaction, while no RN entity is leased. +pub(crate) fn sync_modal_focus(window: &mut Window, cx: &mut App) { + let Some(store) = cx.try_global::().map(|store| store.0.clone()) else { + return; + }; + let (active, descendants, visible, targets, fallback, nodes, focused) = { + let store = store.read(cx); + #[cfg(feature = "visual-capture")] + if std::env::var_os("RN_GPUI_DEBUG_A11Y_TREE").is_some() { + log::info!( + "[accessibility] sync modal tags={:?}", + store.accessibility_modal_tags + ); + } + if store.accessibility_modal_tags.is_empty() + && cx + .try_global::() + .is_none_or(|state| state.active.is_none()) + { + return; + } + let active = store.accessibility_modal_tags.iter().rev().find_map(|tag| { + store + .nodes + .get(tag) + .filter(|node| { + let node = node.read(cx); + !node.view.display_none && !node.view.accessibility_hidden + }) + .map(|node| (*tag, node.clone())) + }); + let mut descendants = HashSet::new(); + let mut targets = Vec::new(); + let mut visible = HashSet::new(); + let mut fallback = None; + if let Some((_, modal)) = &active { + collect_modal(modal, cx, &mut descendants, &mut targets); + visible.clone_from(&descendants); + let modal = modal.read(cx); + fallback = modal.accessibility_focus_handle(); + let mut parent = modal.parent.clone(); + while let Some(entity) = parent.and_then(|node| node.upgrade()) { + let node = entity.read(cx); + visible.insert(node.tag); + parent = node.parent.clone(); + } + } + let focused = store.nodes.iter().find_map(|(tag, node)| { + node.read(cx) + .accessibility_focus_handle() + .filter(|handle| handle.is_focused(window)) + .map(|handle| (*tag, handle)) + }); + ( + active.map(|(tag, _)| tag), + descendants, + visible, + targets, + fallback, + store.nodes.values().cloned().collect::>(), + focused, + ) + }; + if cx.try_global::().is_none() { + cx.set_global(ModalFocusState::default()); + } + let state = cx.global_mut::(); + let changed = state.active != active; + let tree_changed = state.visible != visible; + if changed && state.active.is_none() && active.is_some() { + state.restore = focused; + } + if changed { + state.pending_focus = active.is_some(); + } + let pending_focus = state.pending_focus; + state.active = active; + state.visible = visible; + let restore = if changed && active.is_none() { + state.restore.take() + } else { + None + }; + if changed || tree_changed { + for node in nodes { + node.update(cx, |node, cx| { + node.set_accessibility_tab_stop_allowed( + active.is_none() || descendants.contains(&node.tag), + ); + cx.notify(); + }); + } + window.refresh(); + } + if changed || pending_focus { + if active.is_some() { + if let Some(target) = targets.first().cloned().or(fallback) { + target.focus(window, cx); + cx.global_mut::().pending_focus = false; + } else { + window.on_next_frame(sync_modal_focus); + window.refresh(); + } + } else if let Some((tag, handle)) = restore { + if store.read(cx).nodes.contains_key(&tag) { + handle.focus(window, cx); + } + } + log::info!( + "[accessibility] modal focus active={active:?} restored={}", + active.is_none() + ); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/animation_policy.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/animation_policy.rs new file mode 100644 index 0000000000..d535fa32e4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/animation_policy.rs @@ -0,0 +1,41 @@ +//! Test-only override for platform-owned transitions. +//! +//! This does not change the operating system's reduce-motion preference and +//! does not suppress React Native's frame callbacks (which animated values +//! need in order to reach their final state). + +#[cfg(feature = "test-driver")] +use std::sync::atomic::{AtomicBool, Ordering}; + +#[cfg(feature = "test-driver")] +static DISABLED: AtomicBool = AtomicBool::new(false); + +pub(crate) fn animations_disabled() -> bool { + #[cfg(feature = "test-driver")] + { + return DISABLED.load(Ordering::Relaxed); + } + + #[cfg(not(feature = "test-driver"))] + false +} + +#[cfg(feature = "test-driver")] +pub(crate) fn set_animations_disabled(disabled: bool) { + DISABLED.store(disabled, Ordering::Relaxed); +} + +#[cfg(all(test, feature = "test-driver"))] +mod tests { + use super::{animations_disabled, set_animations_disabled}; + + #[test] + fn test_driver_can_toggle_animations_without_changing_reduce_motion() { + set_animations_disabled(false); + assert!(!animations_disabled()); + set_animations_disabled(true); + assert!(animations_disabled()); + set_animations_disabled(false); + assert!(!animations_disabled()); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/bootstrap.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/bootstrap.rs new file mode 100644 index 0000000000..dd4a8dec94 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/bootstrap.rs @@ -0,0 +1,2345 @@ +use crate::host::Host; +use crate::store::{OverlayHost, StoreGlobal, SurfaceRootView, ViewStore, APP_SURFACE_ID}; +use gpui::{point, px, size, App, AppContext, Application, Bounds, WindowBounds, WindowOptions}; +use rn_gpui_sys::ffi::MountBatch; +use std::cell::Cell; +use std::rc::Rc; +use std::time::Duration; + +// SAFETY: `rn_gpui_mount_batch_applied` is defined in `GpuiMountingManager.cpp` with this signature. +unsafe extern "C" { + fn rn_gpui_mount_batch_applied(); +} + +fn env_string(key: &str, default: &str) -> String { + std::env::var(key).unwrap_or_else(|_| default.to_string()) +} + +/// Smoke mode answers "does the app start and render?" for CI and packaging +/// checks: `run_with` exits with a single result line instead of opening a +/// window for a human. Exit codes: 0 first frame, 1 timeout, 2 JS error. +fn smoke_enabled() -> bool { + matches!( + std::env::var("RN_GPUI_SMOKE").as_deref(), + Ok("1") | Ok("true") + ) +} + +fn smoke_exit(code: i32, message: String) -> ! { + use std::io::Write; + println!("{message}"); + let _ = std::io::stdout().flush(); + let _ = std::io::stderr().flush(); + // `_exit`, not `std::process::exit`: `exit` runs C++ static destructors + // while the JS, network and inspector threads are still running, and one + // of them then calls into a destroyed object ("Pure virtual function + // called", SIGABRT). The result line is already flushed, so nothing is lost. + extern "C" { + fn _exit(code: i32) -> !; + } + // SAFETY: `_exit` ends the process without unwinding, and nothing is borrowed across it. + unsafe { _exit(code) } +} + +fn smoke_first_frame() -> ! { + smoke_exit(0, "RN_GPUI_SMOKE: first frame rendered".to_string()); +} + +/// The last stage: the frame the user is waiting for is on screen. +/// +/// Separate from `smoke_first_frame` because that only runs in smoke mode, and +/// this trace should describe a real launch too. +fn trace_first_frame() { + startup_trace("first frame presented"); +} + +fn smoke_timeout() -> ! { + smoke_exit(1, "RN_GPUI_SMOKE: timeout".to_string()); +} + +fn smoke_js_error(message: &str) -> ! { + smoke_exit(2, format!("RN_GPUI_SMOKE: js error: {message}")); +} + +/// The C++ `onJsError` handler reports a JS error by logging it at error level +/// from `rn-gpui-sys`; that crate logs nothing else at error level except the +/// failure of a `void` TurboModule method. Smoke mode therefore watches for +/// that record and reports the first one before the first frame. +struct SmokeLogger { + inner: env_logger::Logger, +} + +impl log::Log for SmokeLogger { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + self.inner.enabled(metadata) + } + + fn log(&self, record: &log::Record<'_>) { + if record.level() == log::Level::Error && record.target() == "rn_gpui_sys" { + smoke_js_error(&record.args().to_string()); + } + self.inner.log(record); + } + + fn flush(&self) { + self.inner.flush(); + } +} + +/// Installs the process logger. Smoke mode wraps `env_logger` so a JS error can +/// fail the run; otherwise this is the usual one-line init. +fn install_logger(smoke: bool) { + let inner = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .format_timestamp_millis() + .build(); + let max_level = inner.filter(); + if smoke { + log::set_boxed_logger(Box::new(SmokeLogger { inner })).expect("install the smoke logger"); + } else { + log::set_boxed_logger(Box::new(inner)).expect("install the logger"); + } + log::set_max_level(max_level); +} + +/// Starts the smoke deadline. A run that never draws a frame has to fail on its +/// own, because nothing else in the process is watching the clock. +fn smoke_start_timeout() { + let timeout_ms = env_u32("RN_GPUI_SMOKE_TIMEOUT_MS", 60_000); + std::thread::spawn(move || { + std::thread::sleep(Duration::from_millis(u64::from(timeout_ms))); + smoke_timeout(); + }); +} + +/// Reports the first frame that draws React content. A mount batch dirties the +/// window, so the frame GPUI draws after it carries the content, but +/// `on_next_frame` callbacks run *before* that draw. The callback therefore +/// re-arms once and exits at the start of the following frame. +fn smoke_arm_first_frame(cx: &mut gpui::AsyncApp, window: gpui::AnyWindowHandle) { + let armed = cx.update_window( + window, + |_root: gpui::AnyView, window: &mut gpui::Window, _cx: &mut gpui::App| { + window.on_next_frame(|window, _cx| { + window.on_next_frame(|_window, _cx| { + trace_first_frame(); + smoke_first_frame() + }); + }); + }, + ); + if let Err(error) = armed { + log::warn!("could not arm the smoke first-frame callback: {error}"); + } +} + +/// Milliseconds since the process started, measured once. +/// +/// A wall-clock delta from `main` would miss dyld and the C++ static +/// initialisers, which are part of what startup costs. Each platform has a way to +/// ask when the process began; the fallback is simply when this was first called. +fn process_elapsed() -> std::time::Duration { + static START: std::sync::OnceLock = std::sync::OnceLock::new(); + static PROCESS_START: std::sync::OnceLock> = + std::sync::OnceLock::new(); + let now = START.get_or_init(std::time::Instant::now); + let started = PROCESS_START.get_or_init(process_start_time); + match started { + Some(start) => start.elapsed().unwrap_or_else(|_| now.elapsed()), + None => now.elapsed(), + } +} + +/// When the current process started, where the OS will say. +#[cfg(target_os = "macos")] +fn process_start_time() -> Option { + use std::os::raw::c_int; + + // `proc_pidinfo(PROC_PIDTBSDINFO)` fills a struct whose first two fields are + // `pbi_flags` and `pbi_status`; the start time is the pair after them. + #[repr(C)] + #[derive(Default)] + struct ProcBsdInfo { + pbi_flags: u32, + pbi_status: u32, + pbi_xstatus: u32, + pbi_pid: u32, + pbi_ppid: u32, + pbi_uid: u32, + pbi_gid: u32, + pbi_ruid: u32, + pbi_rgid: u32, + pbi_svuid: u32, + // The saved-group id. Named for its position in `proc_bsdinfo`, not read + // here; the layout is what matters for `proc_pidinfo` to fill it. + #[allow(dead_code)] + pbi_saved_gid: u32, + rfu_1: u32, + pbi_comm: [u8; 16], + pbi_name: [u8; 32], + pbi_nfiles: u32, + pbi_pgid: u32, + pbi_pjobc: u32, + e_tdev: u32, + e_tpgid: u32, + pbi_nice: i32, + pbi_start_tvsec: u64, + pbi_start_tvusec: u64, + } + + extern "C" { + fn proc_pidinfo( + pid: c_int, + flavor: c_int, + arg: u64, + buffer: *mut std::os::raw::c_void, + buffersize: c_int, + ) -> c_int; + } + const PROC_PIDTBSDINFO: c_int = 3; + + let mut info = ProcBsdInfo::default(); + let size = std::mem::size_of::() as c_int; + // SAFETY: `info` is a correctly sized, writable struct of the layout the + // call expects, and the return value is checked before it is read. + let written = unsafe { + proc_pidinfo( + std::process::id() as c_int, + PROC_PIDTBSDINFO, + 0, + &mut info as *mut _ as *mut std::os::raw::c_void, + size, + ) + }; + if written != size { + return None; + } + std::time::UNIX_EPOCH.checked_add(std::time::Duration::new( + info.pbi_start_tvsec, + info.pbi_start_tvusec as u32 * 1_000, + )) +} + +#[cfg(target_os = "linux")] +fn process_start_time() -> Option { + // `/proc/self/stat` field 22 is the start time in clock ticks since boot, and + // `/proc/uptime` gives the boot instant's age. Together they date the process. + let stat = std::fs::read_to_string("/proc/self/stat").ok()?; + let start_ticks: u64 = stat + .rsplit_once(") ")? + .1 + .split_whitespace() + .nth(19)? + .parse() + .ok()?; + let uptime: f64 = std::fs::read_to_string("/proc/uptime") + .ok()? + .split_whitespace() + .next()? + .parse() + .ok()?; + // `_SC_CLK_TCK` is 100 on every Linux that matters here. + let started_ago = uptime - (start_ticks as f64 / 100.0); + std::time::SystemTime::now() + .checked_sub(std::time::Duration::from_secs_f64(started_ago.max(0.0))) +} + +#[cfg(target_os = "windows")] +fn process_start_time() -> Option { + #[repr(C)] + #[derive(Default)] + struct FileTime { + low: u32, + high: u32, + } + extern "system" { + fn GetCurrentProcess() -> *mut std::os::raw::c_void; + fn GetProcessTimes( + process: *mut std::os::raw::c_void, + creation: *mut FileTime, + exit: *mut FileTime, + kernel: *mut FileTime, + user: *mut FileTime, + ) -> i32; + } + let mut creation = FileTime::default(); + let mut exit = FileTime::default(); + let mut kernel = FileTime::default(); + let mut user = FileTime::default(); + // SAFETY: all four pointers are to initialised, writable local structs. + let ok = unsafe { + GetProcessTimes( + GetCurrentProcess(), + &mut creation, + &mut exit, + &mut kernel, + &mut user, + ) + }; + if ok == 0 { + return None; + } + // FILETIME counts 100 ns intervals since 1601-01-01. + let ticks = ((creation.high as u64) << 32) | creation.low as u64; + let since_unix = ticks.checked_sub(116_444_736_000_000_000)?; + std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_nanos(since_unix * 100)) +} + +#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] +fn process_start_time() -> Option { + None +} + +/// Emit a startup stage, when `RN_GPUI_STARTUP_TRACE` is set. +/// +/// One cached env check when off, which is the only cost a shipped app pays. On +/// macOS each stage is also an `os_signpost` interval, so Instruments shows the +/// phases by name instead of a flat profile. +fn startup_trace(stage: &str) { + static ENABLED: std::sync::OnceLock = std::sync::OnceLock::new(); + if !*ENABLED.get_or_init(|| std::env::var_os("RN_GPUI_STARTUP_TRACE").is_some()) { + return; + } + let elapsed = process_elapsed(); + eprintln!("[startup] +{} {stage}", elapsed.as_millis()); + signpost_stage(stage, elapsed); +} + +/// Emit the signpost for a stage. +/// +/// The macros in `os/signpost.h` are variadic and expand to a call whose ABI is +/// easy to get wrong from Rust; one wrong argument crashed the app on the very +/// first stage. The C++ side expands them as Apple intends. +fn signpost_stage(stage: &str, elapsed: std::time::Duration) { + rn_gpui_sys::ffi::rn_gpui_startup_signpost(stage, elapsed.as_millis() as u64); +} + +fn window_state_path(app_id: &str) -> std::path::PathBuf { + #[cfg(feature = "visual-capture")] + if let Some(path) = std::env::var_os("RN_GPUI_WINDOW_STATE") { + return path.into(); + } + let safe_id = app_id.replace( + |c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_', + "_", + ); + #[cfg(target_os = "macos")] + let base = std::env::var_os("HOME") + .map(|home| std::path::PathBuf::from(home).join("Library/Application Support")); + #[cfg(target_os = "windows")] + let base = std::env::var_os("APPDATA").map(std::path::PathBuf::from); + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + let base = std::env::var_os("XDG_CONFIG_HOME") + .map(std::path::PathBuf::from) + .or_else(|| { + std::env::var_os("HOME").map(|home| std::path::PathBuf::from(home).join(".config")) + }); + base.unwrap_or_else(std::env::temp_dir) + .join("rn-gpui") + .join(format!("{safe_id}-window.json")) +} + +fn load_window_bounds(path: &std::path::Path) -> Option<(f32, f32, f32, f32)> { + let value: serde_json::Value = serde_json::from_slice(&std::fs::read(path).ok()?).ok()?; + let number = |key| value.get(key)?.as_f64().map(|value| value as f32); + let (x, y, width, height) = ( + number("x")?, + number("y")?, + number("width")?, + number("height")?, + ); + (x.is_finite() + && y.is_finite() + && width.is_finite() + && height.is_finite() + && width >= 320.0 + && height >= 240.0) + .then_some((x, y, width, height)) +} + +fn save_window_bounds( + path: &std::path::Path, + bounds: Bounds, + viewport: gpui::Size, +) { + let value = serde_json::json!({ + "x": f32::from(bounds.origin.x), + "y": f32::from(bounds.origin.y), + // Persist the content size used in `WindowBounds`; GPUI adds the + // platform title-bar inset to `window.bounds()` on restore. + "width": f32::from(viewport.width), + "height": f32::from(viewport.height), + }); + let result = (|| -> std::io::Result<()> { + let parent = path.parent().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "window state path has no parent", + ) + })?; + std::fs::create_dir_all(parent)?; + let temporary = path.with_extension("json.tmp"); + std::fs::write(&temporary, serde_json::to_vec(&value)?)?; + std::fs::rename(temporary, path) + })(); + if let Err(error) = result { + log::warn!( + "could not save window bounds to {}: {error}", + path.display() + ); + } +} + +fn env_f32(key: &str, default: f32) -> f32 { + std::env::var(key) + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(default) +} + +fn env_u32(key: &str, default: u32) -> u32 { + std::env::var(key) + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(default) +} + +/// Whether the runtime should talk to Metro instead of loading `bundle_path`. +/// +/// This is a runtime decision, not a compile-time one. The ReactCommon headers +/// default `enableDevMode` from `REACT_NATIVE_DEBUG`, but the prebuilt runtime +/// is always built in Release, so a debug app that links it would otherwise +/// never reach the dev server. The `rn-gpui` crate is compiled in the app's own +/// Cargo profile, so `debug_assertions` is the right default here and +/// `RN_GPUI_DEV_MODE` overrides it either way. +fn dev_mode() -> bool { + std::env::var("RN_GPUI_DEV_MODE") + .ok() + .map(|value| matches!(value.as_str(), "1" | "true" | "yes")) + .unwrap_or(cfg!(debug_assertions)) +} + +#[cfg(feature = "visual-capture")] +fn env_bool(key: &str, default: bool) -> bool { + std::env::var(key) + .ok() + .map(|value| matches!(value.as_str(), "1" | "true" | "yes")) + .unwrap_or(default) +} + +#[cfg(feature = "visual-capture")] +fn env_opt_f32(key: &str) -> Option { + std::env::var(key).ok().and_then(|value| value.parse().ok()) +} + +#[cfg(feature = "visual-capture")] +fn env_points(key: &str) -> Vec<(f32, f32)> { + std::env::var(key) + .ok() + .map(|value| { + value + .split(';') + .filter_map(|pair| { + let (x, y) = pair.split_once(',')?; + Some((x.trim().parse().ok()?, y.trim().parse().ok()?)) + }) + .collect() + }) + .unwrap_or_default() +} + +#[cfg(feature = "visual-capture")] +fn env_drags(key: &str) -> Vec<(f32, f32, f32, f32, bool)> { + std::env::var(key) + .unwrap_or_default() + .split(';') + .filter_map(|entry| { + let mut parts = entry.split(':'); + let start = parts.next()?; + let end = parts.next()?; + let cancel = parts.next().is_some_and(|phase| phase.trim() == "cancel"); + let (start_x, start_y) = start.split_once(',')?; + let (end_x, end_y) = end.split_once(',')?; + Some(( + start_x.trim().parse().ok()?, + start_y.trim().parse().ok()?, + end_x.trim().parse().ok()?, + end_y.trim().parse().ok()?, + cancel, + )) + }) + .collect() +} + +#[cfg(feature = "visual-capture")] +fn env_scrolls(key: &str) -> Vec<(f32, f32, f32, f32)> { + std::env::var(key) + .unwrap_or_default() + .split(';') + .filter_map(|entry| { + let mut values = entry + .split(',') + .filter_map(|v| v.trim().parse::().ok()); + Some(( + values.next()?, + values.next()?, + values.next()?, + values.next()?, + )) + }) + .collect() +} + +/// macOS stops delivering frames while the display is asleep, so JS work a +/// synthetic click queued would never run; the input loops call this instead. +#[cfg(feature = "visual-capture")] +async fn flush_js(host: &Host, cx: &mut gpui::AsyncApp) { + for _ in 0..6 { + host.on_render(); + cx.background_executor() + .timer(Duration::from_millis(16)) + .await; + } +} + +pub(crate) enum MainEvent { + Mount(MountBatch), + SurfaceStopped(i32), + AnimatedProps(rn_gpui_sys::ffi::AnimatedPropsFlat), + AnimatedActivity(bool), + TextInputCommand { + tag: i32, + focus: bool, + }, + AccessibilityFocus(i32), + TextInputClear(i32), + /// A command Fabric sent to a host component: its index in the spec's + /// `COMMANDS` and its arguments, already validated by the generated C++ + /// dispatcher. + HostCommand { + tag: i32, + command: u32, + args: Vec, + }, + DevMenuShow, + DevSetting { + name: String, + enabled: bool, + }, + DevMenuPerform(String), + TrayClick { + x: f32, + y: f32, + width: f32, + height: f32, + button: u8, + }, + TrayMenuPerform(String), + MenuBarPerform(String), + /// One desktop-shell command, run with the app context the main loop holds. + /// It answers through the channel it was built with, which may be later: + /// a context menu answers when it closes. + DesktopCommand(Box), + MainTask(u64), + /// Work a Rust module asked to run on the main thread. + RustJob(rn_gpui_module::Job), +} + +pub type WindowOptionsHook = Box; +pub type AppReadyHook = Box; + +pub type WindowOpenedHook = Box; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WindowTitleBar { + #[default] + Default, + Transparent, + /// No system title bar at all. GPUI only has "transparent" on macOS, so + /// this is the same as [`WindowTitleBar::Transparent`] there. + Hidden, +} + +impl WindowTitleBar { + /// What a `desktop.json` value or a `titleBar` prop names. + /// + /// An unknown name falls back to the default bar rather than failing: the + /// schema is what rejects a typo in `desktop.json`, and a secondary window + /// opened by an app built against an older schema must still open. + pub fn from_name(name: &str) -> Self { + match name { + "transparent" => Self::Transparent, + "hidden" => Self::Hidden, + _ => Self::Default, + } + } + + /// Whether the app draws the bar itself, which is what lets the platform + /// drop its own chrome. + pub fn is_custom(self) -> bool { + !matches!(self, Self::Default) + } +} + +/// Keeps the traffic lights inside the window they belong to. +/// +/// A position from `desktop.json` is app-authored, so it can name somewhere the +/// window does not reach; AppKit would then move the buttons off screen with no +/// way to drag them back. A non-finite coordinate is treated as the origin rather +/// than propagated, because `f32::clamp` panics on `NaN`. +pub fn clamp_traffic_light_position(x: f32, y: f32, width: f32, height: f32) -> (f32, f32) { + fn clamp(value: f32, limit: f32) -> f32 { + if !value.is_finite() || !(limit > 0.0) { + 0.0 + } else { + value.clamp(0.0, limit) + } + } + (clamp(x, width), clamp(y, height)) +} + +/// Builds the GPUI view that is a GPUI-first app's main window content. +type GpuiRootHook = Box gpui::AnyView>; + +pub struct DesktopAppConfig { + pub app_id: String, + pub module: String, + /// Name of the installed JS engine package to run, e.g. `"hermes"`. Empty + /// uses the only installed engine. `RN_GPUI_JS_ENGINE` overrides this. + pub js_engine: String, + pub width: f32, + pub height: f32, + pub title: Option, + pub position: Option<(f32, f32)>, + pub restore_window_state: bool, + pub quit_on_last_window_closed: bool, + pub title_bar: WindowTitleBar, + /// Whether the Dev Menu is available. `None` follows the build: on in a + /// debug build, off in a release build. + pub dev_menu: Option, + pub tray: Option, + pub traffic_light_position: Option<(f32, f32)>, + /// Whether the main window shows a menu bar. Windows and Linux only. + pub menu_bar: bool, + pub configure_window: Option, + pub on_app_ready: Option, + + pub on_window_opened: Option, + /// A GPUI-first app's main window content. When set, + /// the main window shows this view instead of starting `module` on surface 1; + /// React content lives in the `ReactSurface`s the view places. + pub gpui_root: Option, +} + +impl DesktopAppConfig { + pub fn from_env() -> Self { + Self::from_env_with_defaults("App", "com.example.rngpui") + } + + pub fn from_env_with_defaults(module: &str, app_id: &str) -> Self { + Self { + app_id: env_string("RN_GPUI_APP_ID", app_id), + module: env_string("RN_GPUI_MODULE", module), + js_engine: String::new(), + width: env_f32("RN_GPUI_WIDTH", 800.0), + height: env_f32("RN_GPUI_HEIGHT", 600.0), + title: None, + position: None, + restore_window_state: true, + quit_on_last_window_closed: true, + title_bar: WindowTitleBar::Default, + dev_menu: None, + tray: None, + traffic_light_position: None, + menu_bar: true, + configure_window: None, + on_app_ready: None, + on_window_opened: None, + gpui_root: None, + } + } + + #[must_use] + pub fn js_engine(mut self, name: impl Into) -> Self { + self.js_engine = name.into(); + self + } + + #[must_use] + pub fn tray(mut self, config: crate::desktop::tray::TrayConfig) -> Self { + self.tray = Some(config); + self + } + + #[must_use] + pub fn dev_menu(mut self, enabled: bool) -> Self { + self.dev_menu = Some(enabled); + self + } + + #[must_use] + pub fn window_title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + #[must_use] + pub fn window_size(mut self, width: f32, height: f32) -> Self { + self.width = width.max(320.0); + self.height = height.max(240.0); + self + } + + #[must_use] + pub fn window_position(mut self, x: f32, y: f32) -> Self { + self.position = Some((x, y)); + self + } + + #[must_use] + pub fn restore_window_state(mut self, restore: bool) -> Self { + self.restore_window_state = restore; + self + } + + #[must_use] + pub fn quit_on_last_window_closed(mut self, quit: bool) -> Self { + self.quit_on_last_window_closed = quit; + self + } + + #[must_use] + pub fn title_bar(mut self, title_bar: WindowTitleBar) -> Self { + self.title_bar = title_bar; + self + } + + #[must_use] + pub fn traffic_light_position(mut self, x: f32, y: f32) -> Self { + self.traffic_light_position = Some((x, y)); + self + } + + /// Whether the main window shows a menu bar. + /// + /// Only Windows and Linux draw one inside the window; on macOS the bar is the + /// system's, so this is ignored there. + #[must_use] + pub fn menu_bar(mut self, visible: bool) -> Self { + self.menu_bar = visible; + self + } + + #[must_use] + pub fn configure_window( + mut self, + configure: impl FnOnce(&mut WindowOptions) + 'static, + ) -> Self { + self.configure_window = Some(Box::new(configure)); + self + } + + #[must_use] + pub fn on_app_ready(mut self, callback: impl FnOnce(&mut App) + 'static) -> Self { + self.on_app_ready = Some(Box::new(callback)); + self + } + + #[must_use] + pub fn on_window_opened( + mut self, + callback: impl FnOnce(gpui::WindowId, &mut gpui::Window, &mut App) + 'static, + ) -> Self { + self.on_window_opened = Some(Box::new(callback)); + self + } + + /// Makes this a GPUI-first app: the main window shows the view `build` + /// returns, and React content appears where that view places a + /// [`ReactSurface`](crate::ReactSurface). + /// + /// The runtime still boots the way `run_with` always does — the JS engine, the + /// bundle, the modules, the store — but no React tree is started on the main + /// surface, so the window belongs to the app. `build` runs once, with the + /// [`ReactRuntime`](crate::ReactRuntime) it can open further windows through. + /// + /// ```ignore + /// run_with(DesktopAppConfig::from_env().gpui_first(|cx, _runtime| { + /// cx.new(|cx| { + /// let details = cx.new(|cx| ReactSurface::new("Details", cx)); + /// Root { details } + /// }) + /// .into() + /// })); + /// ``` + #[must_use] + pub fn gpui_first( + mut self, + build: impl FnOnce(&mut App, crate::runtime::ReactRuntime) -> gpui::AnyView + 'static, + ) -> Self { + self.gpui_root = Some(Box::new(build)); + self + } +} + +impl Default for DesktopAppConfig { + fn default() -> Self { + Self::from_env() + } +} + +pub fn run() { + run_with(DesktopAppConfig::from_env()); +} + +pub fn run_with(mut config: DesktopAppConfig) { + #[cfg(not(feature = "test-driver"))] + if std::env::var_os("RN_GPUI_TEST_DRIVER").is_some_and(|value| value != "0") { + panic!("RN_GPUI_TEST_DRIVER is enabled, but this app was not built with --features test-driver"); + } + startup_trace("main entered"); + rn_gpui_sys::note_main_thread(); + rn_gpui_module::note_main_thread(); + let smoke = smoke_enabled(); + install_logger(smoke); + if smoke { + smoke_start_timeout(); + } + + // A packaged release build has no source tree: the bundle is beside the + // executable. `RN_GPUI_BUNDLE`, which the CLI sets in development, still + // wins, and a debug build bakes in the path it was developed against. + // + // A release build with neither is a packaging mistake — no resources were + // installed — so the fallback names the file it looked for rather than a + // path into some checkout. That path must never be a repository path: it + // would resolve on the machine that built the app, and nowhere else. + let bundle = crate::resources::default_bundle("index.bundle"); + let source_url = env_string("RN_GPUI_SOURCE_URL", "index"); + let app_id = config.app_id.clone(); + let js_engine = env_string("RN_GPUI_JS_ENGINE", &config.js_engine); + let dev_host = env_string("RN_GPUI_DEV_HOST", "localhost"); + let dev_port = env_u32("RN_GPUI_DEV_PORT", 8081); + let module = config.module.clone(); + let width = config.width; + let height = config.height; + let window_state_path = window_state_path(&app_id); + #[cfg(feature = "visual-capture")] + let scale_override = env_opt_f32("RN_GPUI_SCALE"); + #[cfg(not(feature = "visual-capture"))] + let scale_override: Option = None; + #[cfg(feature = "visual-capture")] + let run_ms = env_u32("RN_GPUI_RUN_MS", 0); + #[cfg(feature = "perf-timing")] + let benchmark_ms = env_u32("RN_GPUI_BENCHMARK_MS", 0); + #[cfg(feature = "perf-timing")] + let benchmark_mode = crate::store::benchmark_mode() + .unwrap_or("unspecified") + .to_owned(); + #[cfg(feature = "visual-capture")] + let resize_ms = env_u32("RN_GPUI_RESIZE_MS", 0); + #[cfg(feature = "visual-capture")] + let resize_width = env_f32("RN_GPUI_RESIZE_WIDTH", 1200.0); + #[cfg(feature = "visual-capture")] + let resize_height = env_f32("RN_GPUI_RESIZE_HEIGHT", 400.0); + #[cfg(feature = "visual-capture")] + let clicks = env_points("RN_GPUI_CLICK"); + #[cfg(feature = "visual-capture")] + let click_ms = env_u32("RN_GPUI_CLICK_MS", 1500); + #[cfg(feature = "visual-capture")] + let click_hold_ms = env_u32("RN_GPUI_CLICK_HOLD_MS", 0); + #[cfg(feature = "visual-capture")] + let drags = env_drags("RN_GPUI_DRAG"); + #[cfg(feature = "visual-capture")] + let drag_ms = env_u32("RN_GPUI_DRAG_MS", 1800); + #[cfg(feature = "visual-capture")] + let drag_start_ms = env_u32("RN_GPUI_DRAG_START_MS", drag_ms); + #[cfg(feature = "visual-capture")] + let skip_synthetic_input_flush = env_bool("RN_GPUI_SKIP_INPUT_FLUSH", false); + #[cfg(feature = "visual-capture")] + let hovers = env_points("RN_GPUI_HOVER"); + #[cfg(feature = "visual-capture")] + let hover_ms = env_u32("RN_GPUI_HOVER_MS", 800); + #[cfg(feature = "visual-capture")] + let scrolls = env_scrolls("RN_GPUI_SCROLL"); + #[cfg(feature = "visual-capture")] + let scroll_ms = env_u32("RN_GPUI_SCROLL_MS", 1500); + #[cfg(feature = "visual-capture")] + let scroll_start_ms = env_u32("RN_GPUI_SCROLL_START_MS", scroll_ms); + #[cfg(feature = "visual-capture")] + let keys: Vec = std::env::var("RN_GPUI_KEY") + .unwrap_or_default() + .split(';') + .map(str::trim) + .filter(|key| !key.is_empty()) + .map(str::to_owned) + .collect(); + #[cfg(feature = "visual-capture")] + let key_ms = env_u32("RN_GPUI_KEY_MS", 1500); + #[cfg(feature = "visual-capture")] + let mouse_navigation: Vec = std::env::var("RN_GPUI_MOUSE_NAV") + .unwrap_or_default() + .split(';') + .map(str::trim) + .filter(|direction| matches!(*direction, "back" | "forward")) + .map(str::to_owned) + .collect(); + #[cfg(feature = "visual-capture")] + let mouse_navigation_ms = env_u32("RN_GPUI_MOUSE_NAV_MS", 1500); + #[cfg(feature = "visual-capture")] + let screenshot = std::env::var("RN_GPUI_SCREENSHOT").ok(); + #[cfg(feature = "visual-capture")] + let screenshot_delay_ms = env_u32("RN_GPUI_SCREENSHOT_DELAY_MS", 500); + #[cfg(feature = "visual-capture")] + let screenshots: Vec<(u32, String)> = std::env::var("RN_GPUI_SCREENSHOTS") + .unwrap_or_default() + .split(';') + .filter_map(|entry| { + let (delay, path) = entry.split_once(':')?; + Some((delay.parse().ok()?, path.to_owned())) + }) + .collect(); + + // Installed before any surface starts, otherwise the first mount batch would + // be dropped. + let (tx, rx) = async_channel::unbounded::(); + let mount_tx = tx.clone(); + let _ = rn_gpui_sys::MOUNT_HANDLER.set(Box::new(move |batch: MountBatch| { + let _ = mount_tx.send_blocking(MainEvent::Mount(batch)); + })); + let surface_tx = tx.clone(); + let _ = rn_gpui_sys::SURFACE_STOPPED_HANDLER.set(Box::new(move |surface_id: i32| { + let _ = surface_tx.send_blocking(MainEvent::SurfaceStopped(surface_id)); + })); + let animated_tx = tx.clone(); + let _ = rn_gpui_sys::ANIMATED_PROPS_HANDLER.set(Box::new(move |props| { + let _ = animated_tx.send_blocking(MainEvent::AnimatedProps(props)); + })); + let activity_tx = tx.clone(); + let _ = rn_gpui_sys::ANIMATED_ACTIVE_HANDLER.set(Box::new(move |active| { + let _ = activity_tx.send_blocking(MainEvent::AnimatedActivity(active)); + })); + let text_input_command_tx = tx.clone(); + let _ = rn_gpui_sys::TEXT_INPUT_COMMAND_HANDLER.set(Box::new(move |tag, focus| { + let _ = text_input_command_tx.send_blocking(MainEvent::TextInputCommand { tag, focus }); + })); + let host_command_tx = tx.clone(); + let _ = rn_gpui_sys::HOST_COMMAND_HANDLER.set(Box::new(move |tag, command, args| { + let _ = host_command_tx.send_blocking(MainEvent::HostCommand { + tag, + command, + args: args + .into_iter() + .map(crate::host_component::HostArg::from_ffi) + .collect(), + }); + })); + let accessibility_focus_tx = tx.clone(); + let _ = rn_gpui_sys::ACCESSIBILITY_FOCUS_HANDLER.set(Box::new(move |tag| { + let _ = accessibility_focus_tx.send_blocking(MainEvent::AccessibilityFocus(tag)); + })); + let text_input_clear_tx = tx.clone(); + let _ = rn_gpui_sys::TEXT_INPUT_CLEAR_HANDLER.set(Box::new(move |tag| { + let _ = text_input_clear_tx.send_blocking(MainEvent::TextInputClear(tag)); + })); + let desktop_tx = tx.clone(); + crate::desktop::set_command_sender(desktop_tx); + let main_task_tx = tx.clone(); + let _ = rn_gpui_sys::MAIN_TASK_HANDLER.set(Box::new(move |id| { + let _ = main_task_tx.send_blocking(MainEvent::MainTask(id)); + })); + // Rust modules that ask for the main thread hand their work over here; the + // future is created and polled on the main thread, so it never has to be + // `Send`. + let rust_job_tx = tx.clone(); + rn_gpui_module::set_main_spawner(move |job| { + let _ = rust_job_tx.send_blocking(MainEvent::RustJob(job)); + }); + let dev_menu_tx = tx.clone(); + let _ = rn_gpui_sys::DEV_MENU_SHOW_HANDLER.set(Box::new(move || { + let _ = dev_menu_tx.send_blocking(MainEvent::DevMenuShow); + })); + let dev_setting_tx = tx.clone(); + let _ = rn_gpui_sys::DEV_SETTING_HANDLER.set(Box::new(move |name, enabled| { + let _ = dev_setting_tx.send_blocking(MainEvent::DevSetting { name, enabled }); + })); + let dev_perform_tx = tx.clone(); + let _ = rn_gpui_sys::DEV_MENU_PERFORM_HANDLER.set(Box::new(move |id| { + let _ = dev_perform_tx.send_blocking(MainEvent::DevMenuPerform(id)); + })); + let tray_click_tx = tx.clone(); + let _ = rn_gpui_sys::TRAY_CLICK_HANDLER.set(Box::new(move |x, y, width, height, button| { + let _ = tray_click_tx.send_blocking(MainEvent::TrayClick { + x, + y, + width, + height, + button, + }); + })); + let menu_bar_tx = tx.clone(); + let _ = rn_gpui_sys::MENU_BAR_PERFORM_HANDLER.set(Box::new(move |id| { + let _ = menu_bar_tx.send_blocking(MainEvent::MenuBarPerform(id)); + })); + let tray_menu_tx = tx; + let _ = rn_gpui_sys::TRAY_MENU_PERFORM_HANDLER.set(Box::new(move |id| { + let _ = tray_menu_tx.send_blocking(MainEvent::TrayMenuPerform(id)); + })); + // The snapshot is read on the JS thread during module initialization, so it + // has to be installed before the bundle loads. + crate::desktop::install_snapshot(&app_id); + // The desktop shell is a Rust TurboModule: JS asks for it by name. + crate::desktop::module::register(); + crate::builtins::register(); + + #[cfg(feature = "visual-capture")] + let headless = env_bool("RN_GPUI_HEADLESS", false); + #[cfg(not(feature = "visual-capture"))] + let headless = false; + startup_trace(if headless { + "using GPUI headless platform" + } else { + "using GPUI AppKit platform" + }); + let platform = gpui_platform::current_platform(headless); + startup_trace("platform created"); + // A locked or absent session on macOS cannot launch AppKit, and the platform + // reports no display until it can. Nothing else does: on Linux GPUI starts on + // a placeholder connection and attaches X11 or Wayland once the app runs, so + // its display list is legitimately empty here — waiting for it never ends. + #[cfg(target_os = "macos")] + { + let mut waited_for_display = 0u64; + while platform.displays().is_empty() { + if waited_for_display == 0 { + log::warn!("no active display; waiting before entering AppKit launch"); + startup_trace("waiting for an active display"); + } + std::thread::sleep(Duration::from_secs(2)); + waited_for_display += 2; + if waited_for_display % 30 == 0 { + log::warn!("still waiting for an active display ({waited_for_display}s)"); + } + } + } + startup_trace("active display detected"); + let application = Application::with_platform(platform); + // Before any request can arrive: the C++ bridge reports completions through + // this handler, and it is registered once. + crate::http_client::install(); + application.on_reopen(crate::desktop::windows::reopen); + application + .with_http_client(std::sync::Arc::new(crate::http_client::RnHttpClient::new())) + .run(move |cx: &mut App| { + startup_trace("application run"); + crate::image_loader::install(cx); + crate::desktop::icon::install(cx); + crate::text::install_fonts(cx); + // Installed before the host starts a surface: Fabric measures paragraphs + // from the JS thread and would otherwise fall back to a zero-size stub. + crate::text::install_measure_handler(cx); + if let Some(icon) = std::env::var_os("RN_GPUI_APP_ICON") { + crate::desktop::platform_window::set_dock_icon(std::path::Path::new(&icon)); + } + // Has to happen before the first window opens. + if let Some(tray) = &config.tray { + if tray.accessory { + crate::desktop::platform_window::set_accessory_app(true); + } + } + if let Some(on_app_ready) = config.on_app_ready.take() { + on_app_ready(cx); + } + + // Both must exist before the main window opens: the registry adopts it + // below, and the close hook reads the policy while the window is live. + cx.set_global(crate::desktop::windows::DesktopWindowsGlobal( + crate::desktop::windows::DesktopWindows::new(), + )); + cx.set_global(crate::desktop::windows::QuitOnLastWindowClosed( + config.quit_on_last_window_closed, + )); + // Installed before the bundle loads so a startup failure still leaves a + // working menu bar. + let menu_app_name = config + .title + .clone() + .unwrap_or_else(|| config.module.clone()); + let dev_menu = config.dev_menu.unwrap_or(cfg!(debug_assertions)); + crate::desktop::dev_menu::install(cx, dev_menu); + crate::desktop::app_menu::install(cx, &menu_app_name, dev_menu); + crate::desktop::context_menu::install(cx); + // The tray icon needs a running NSApplication, so it is created here + // rather than while parsing the config. + if let Some(tray) = config.tray.take() { + if let Err(error) = crate::desktop::tray::install(cx, tray) { + log::error!("[desktop][tray] {error}"); + } + } + crate::desktop::perf_monitor::install(cx); + // Shortcut bindings are ordinary keymap bindings; this only installs the + // handler for the ones with no scope element, which is the last stop + // after every element on the focus path had its turn. + crate::desktop::keymap::register(cx); + + // The overlay slot (LogBox) has to be in the app root's element tree from the + // first frame: GPUI only re-renders entities the window tracked while drawing. + let overlay_host = cx.new(|_| OverlayHost::new()); + let root_view = SurfaceRootView::new(APP_SURFACE_ID).with_overlay_host(overlay_host.clone()); + #[cfg(all(target_os = "macos", feature = "visual-capture"))] + let root_view = if env_bool("RN_GPUI_NATIVE_WEBVIEW_PROBE", false) { + root_view.with_native_view_probe( + crate::native_view_probe::create_webview_probe_entity(cx), + ) + } else if env_bool("RN_GPUI_NATIVE_COMPOSITION_PROBE", false) { + root_view.with_native_view_probe( + crate::native_view_probe::create_probe_entity(cx), + ) + } else { + root_view + }; + let root = cx.new(|_| root_view); + let restored_bounds = config + .restore_window_state + .then(|| load_window_bounds(&window_state_path)) + .flatten(); + startup_trace("window state loaded"); + let bounds = restored_bounds + .map(|(x, y, width, height)| Bounds::new(point(px(x), px(y)), size(px(width), px(height)))) + .unwrap_or_else(|| { + let size = size(px(width), px(height)); + if let Some((x, y)) = config.position { + Bounds::new(point(px(x), px(y)), size) + } else { + Bounds::centered(None, size, cx) + } + }); + #[cfg(feature = "test-driver")] + let bounds = if env_bool("RN_GPUI_TEST_OFFSCREEN", false) { + Bounds::new(point(px(-10000.0), px(-10000.0)), bounds.size) + } else { bounds }; + let (width, height) = restored_bounds.map_or((width, height), |(_, _, width, height)| (width, height)); + if let Some((x, y, restored_width, restored_height)) = restored_bounds { + log::info!("restoring window at ({x:.0},{y:.0} {restored_width:.0}x{restored_height:.0})"); + } + // Start React Native before the window exists. + // + // JS runs on its own thread, and a real app's startup is 50-300 ms that + // used to happen entirely after AppKit finished. The size and scale here + // are the values the window is about to be created with; anything the + // window alone can report (its top inset, its final scale) is applied + // once it exists, through the same path a resize takes. + // GPUI reports the scale from a window, and there is no window yet. + // `RN_GPUI_SCALE` overrides it for tests; otherwise the estimate is the + // Retina default on macOS and 1.0 elsewhere. The surface is corrected + // below if the window disagrees, so a wrong guess costs one resize, not a + // wrong layout. + let estimated_scale = scale_override.unwrap_or(if cfg!(target_os = "macos") { + 2.0 + } else { + 1.0 + }); + // A surface must not start at zero: `RN_GPUI_WIDTH=0` would otherwise + // reach JS as a 0x0 viewport, and Fabric would lay out the whole tree in + // no space at all. The window's own size is authoritative once it opens. + let surface_width = width.max(320.0); + let surface_height = height.max(240.0); + let host = Host::new(&app_id, &dev_host, dev_port, &js_engine, dev_mode()); + startup_trace("host created"); + host.set_window_metrics(surface_width, surface_height, estimated_scale); + startup_trace("bundle load start"); + if !host.load_script(&bundle, &source_url) { + log::error!("load_script failed for {bundle}"); + std::process::exit(1); + } + startup_trace("bundle load end"); + log::info!("loaded {bundle}"); + // The surface starts now as well: mount batches queue on the main-thread + // channel until the store and its window exist, which is the same path a + // background surface already uses. + let menu_bar_inset = crate::desktop::gpui_menu::menu_bar_inset(); + let gpui_first = config.gpui_root.is_some(); + if let Some(build) = config.gpui_root.take() { + let view = build(cx, crate::runtime::ReactRuntime::new(estimated_scale)); + root.update(cx, |root, _cx| root.set_app_content(view)); + } else { + host.start_surface( + 1, + &module, + surface_width, + (surface_height - menu_bar_inset).max(1.0), + estimated_scale, + ); + } + startup_trace("surface started"); + if !gpui_first { + cx.set_global(crate::desktop::windows::ReopenWindow( + crate::desktop::windows::OpenWindowOptions { + component: module.clone(), + title: Some(config.title.clone().unwrap_or_else(|| module.clone())), + width: Some(width), + height: Some(height), + resizable: true, + title_bar: config.title_bar, + traffic_light_position: config.traffic_light_position, + ..Default::default() + }, + )); + } + + startup_trace("opening GPUI window"); + let mut window_options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }; + if config.title_bar.is_custom() { + let titlebar = window_options.titlebar.get_or_insert_with(Default::default); + // macOS and Windows both express "no system chrome" as a transparent + // title bar. Linux has no such flag: it asks the compositor for + // client-side decorations instead, which is the same outcome — the + // window keeps no title bar and the app draws and moves its own. + titlebar.appears_transparent = true; + if let Some((x, y)) = config.traffic_light_position { + let (x, y) = clamp_traffic_light_position(x, y, width, height); + titlebar.traffic_light_position = Some(point(px(x), px(y))); + } + // The app draws and moves the bar itself, so AppKit must not also + // treat the content view as titlebar chrome. + window_options.app_owns_titlebar_drag = true; + // Ignored on macOS and Windows; on X11 and Wayland it is what + // removes the server-side frame. A compositor that cannot do + // client-side decorations falls back to the server's, and + // `window.window_decorations()` then says so. + window_options.window_decorations = Some(gpui::WindowDecorations::Client); + } + #[cfg(feature = "test-driver")] + if env_bool("RN_GPUI_TEST_OFFSCREEN", false) { + // AppKit clamps remote coordinates into the visible desktop. + // Hidden windows still support forced draws and injected input. + window_options.show = false; + window_options.focus = false; + } + if let Some(configure_window) = config.configure_window.take() { + configure_window(&mut window_options); + } + #[cfg(feature = "visual-capture")] + if std::env::var_os("RN_GPUI_TRANSPARENT_TITLEBAR").is_some() { + window_options.titlebar.get_or_insert_with(Default::default).appears_transparent = true; + } + #[cfg(target_os = "windows")] + let opens_hidden = !window_options.show; + let window = cx + .open_window( + window_options, + { + let root = root.clone(); + move |_window, _cx| root + }, + ) + .expect("failed to open the GPUI window"); + startup_trace("window open"); + // Set per window, not once: a dev run's executable carries no icon, and + // on Windows the icon belongs to the window, not the application. Windows + // opened later get it in `windows::open_window`. + let _ = window.update(cx, |_root, window, _cx| { + crate::desktop::platform_window::apply_app_icon(window); + }); + let window_id = window.window_id(); + // What the compositor actually gave, which is the only authority on + // whether the request for client-side decorations was granted. A window + // manager that ignored it kept its own frame, and the app then has to + // know not to draw a second set of caption buttons beside it. + let applied_decorations = window + .update(cx, |_root, window, _cx| window.window_decorations()) + .ok(); + if let Some(decorations) = applied_decorations { + crate::desktop::capabilities::note_window_decorations(decorations); + } + + // A window created with `show: false` never receives the message that + // carries its initial size on Windows, and GPUI creates the HWND with + // `CW_USEDEFAULT`, so `window.bounds()` reports that sentinel + // (`-1717986944`) until something resizes the window. Laying out at that + // size fails React Native's own assertion (`minimumSize.width >= 0`) and + // kills the process. Resizing delivers the `WM_SIZE` a shown window + // would have received at creation, which is what publishes the real + // size to the bounds observers. `bounds_changed` is deliberately not + // called here: it would lay the surface out at the sentinel size before + // that message arrives. + #[cfg(target_os = "windows")] + if opens_hidden { + let requested = bounds.size; + let applied = window.update(cx, move |_root, window, _cx| { + if window.viewport_size() != requested { + window.resize(requested); + } + }); + if let Err(error) = applied { + log::warn!("could not size the hidden window: {error}"); + } + } + + if config.title.is_some() || config.on_window_opened.is_some() { + let title = config.title.take(); + let on_window_opened = config.on_window_opened.take(); + window + .update(cx, move |_root, window, cx| { + if let Some(title) = title.as_deref() { + window.set_window_title(title); + } + if let Some(on_window_opened) = on_window_opened { + on_window_opened(window_id, window, cx); + } + }) + .expect("failed to configure the opened GPUI window"); + } + + let window_bounds = window + .update(cx, |_root, window, _cx| { + let bounds = window.bounds(); + ( + f32::from(bounds.origin.x), + f32::from(bounds.origin.y), + f32::from(bounds.size.width), + f32::from(bounds.size.height), + ) + }) + .map(|bounds| { + format!( + "({:.0},{:.0} {:.0}x{:.0})", + bounds.0, bounds.1, bounds.2, bounds.3 + ) + }) + .unwrap_or_else(|error| format!("")); + log::info!("window {window_id:?} open at {window_bounds}"); + + #[cfg(any(feature = "visual-capture", feature = "perf-timing"))] + let any_window = *window; + + // React Native is already running; the window supplies the three values + // only it knows, and the surface is corrected if the estimate was wrong. + let window_scale = window + .update(cx, |_root, window, _cx| window.scale_factor()) + .unwrap_or(estimated_scale); + let scale = scale_override.unwrap_or(window_scale); + + let top_inset = window.update(cx, |_root, window, _cx| { + #[cfg(target_os = "macos")] + { crate::native_view::window_top_inset(window) } + #[cfg(not(target_os = "macos"))] + { 0.0 } + }).unwrap_or(0.0); + host.set_window_top_inset(top_inset); + log::info!("[window-inset] native window top inset={top_inset}"); + let appearance = match cx.window_appearance() { + gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => "dark", + _ => "light", + }; + host.set_system_appearance(appearance); + // The window may have opened at a different size or scale than the + // estimate — a restore onto another display, or a maximized window — so + // the surface is told, through the path a resize takes. + // A wrong estimate is corrected here, not tolerated: JS reads Dimensions at + // module initialization, so a surface told the wrong size or scale lays out + // against it until something resizes it. Size is checked as well as scale, + // because they are wrong for different reasons — the scale is a guess from + // the platform, the size comes from the config or the restored window + // state, and a window manager can override either. + let size_moved = (width - surface_width).abs() > f32::EPSILON + || (height - surface_height).abs() > f32::EPSILON; + if size_moved || (window_scale - estimated_scale).abs() > f32::EPSILON { + log::info!( + "surface corrected {surface_width}x{surface_height}@{estimated_scale}x -> \ + {width}x{height}@{window_scale}x after the window opened" + ); + if gpui_first { + // No React surface: only `Dimensions` has to learn the values. + host.set_window_metrics(width, height, scale); + } else { + // The surface itself, not only `Dimensions`. It was started at the + // estimated scale, and Yoga rounds the layout to that scale's pixel + // grid: told only about `Dimensions`, a surface started at 1x kept + // its 1x layout, so a 1230.4-point window laid out 1230 points — + // 1537.5 pixels at 1.25x — and the window's last pixel column was + // never painted until something resized it. `set_surface_size` is + // what a resize calls, and it updates `Dimensions` as well. + host.set_surface_size( + APP_SURFACE_ID, + width, + (height - menu_bar_inset).max(1.0), + scale, + ); + } + } + if gpui_first { + log::info!( + "GPUI-first app: no React tree on surface 1 (window scale {window_scale})" + ); + } else { + log::info!( + "started surface 1 ({module}) at {width}x{}@{scale}x (window scale {window_scale}, \ + menu bar inset {menu_bar_inset})", + height - menu_bar_inset + ); + } + + let frame_hook = crate::frame::FrameHook::new(host.clone()); + window + .update(cx, |_root, window, _cx| frame_hook.arm(window)) + .expect("failed to arm the per-frame hook"); + let appearance_host = host.clone(); + let appearance_subscription = window + .update(cx, |_root, window, _cx| { + window.observe_window_appearance(move |window, _cx| { + let appearance = match window.appearance() { + gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => "dark", + _ => "light", + }; + appearance_host.set_system_appearance(appearance); + }) + }) + .expect("failed to observe window appearance"); + + #[cfg(feature = "visual-capture")] + let host_for_input = host.clone(); + let store = cx.new(|_| ViewStore::new(host, window, frame_hook.clone(), overlay_host)); + cx.set_global(StoreGlobal(store.clone())); + #[cfg(feature = "test-driver")] + if std::env::var("RN_GPUI_TEST_DRIVER").as_deref() == Ok("1") { + let animations_disabled = std::env::var("RN_GPUI_TEST_DISABLE_ANIMATIONS") + .map(|value| matches!(value.as_str(), "1" | "true" | "yes")) + .unwrap_or(false); + crate::animation_policy::set_animations_disabled(animations_disabled); + crate::test_driver::start(cx, store.clone(), (*window).into()); + } + let keyboard_host = store.read(cx).host.clone(); + let keyboard_hook = frame_hook.clone(); + let navigation_keyboard_subscription = cx.intercept_keystrokes(move |event, window, cx| { + if window.window_handle().window_id() != window_id { return; } + if let Some(event_name) = crate::builtins::textinput::input::desktop_navigation_event(&event.keystroke) { + keyboard_host.emit_device_event(event_name); + keyboard_hook.arm(window); + log::info!("[navigation] emitted {event_name} from keyboard"); + cx.stop_propagation(); + } + }); + + crate::desktop::perf_monitor::attach(cx, &root, 1); + // The menu bar is prepared inside a window update (it needs the window + // for the activation observer) and attached to the root afterwards, + // outside any update that has already leased the root. + // `window.menuBar: false` hides the bar before it is ever drawn, so the + // window opens at the right size instead of shrinking once the bar goes. + if !config.menu_bar { + crate::desktop::gpui_menu::set_menu_bar_visible( + crate::desktop::windows::MAIN_WINDOW_KEY, + false, + ); + } + let menu_bar = window + .update(cx, |_root, window, cx| { + crate::desktop::gpui_menu::prepare(window, cx, 1) + }) + .ok() + .flatten(); + if let Some(prepared) = menu_bar { + crate::desktop::gpui_menu::attach(cx, &root, prepared); + } + store.update(cx, |store, cx| { + store.register_root(1, root); + store.top_inset = top_inset; + store.refresh_inherited(1, cx); + store.navigation_keyboard_subscription = Some(navigation_keyboard_subscription); + store.window_appearance_subscription = Some(appearance_subscription); + cx.notify(); + }); + + cx.global_mut::() + .0 + .adopt( + crate::desktop::windows::MAIN_WINDOW_KEY, + (*window).into(), + 1, + module.clone(), + serde_json::json!({}), + crate::desktop::windows::WindowRole::Main, + ); + + // Without the close hook `closeRequested` never fires for the main window. + crate::desktop::windows::install_close_hook_for( + cx, + (*window).into(), + crate::desktop::windows::MAIN_WINDOW_KEY, + ); + + #[cfg(feature = "visual-capture")] + if !clicks.is_empty() { + let host = host_for_input.clone(); + cx.spawn(async move |cx| { + for (x, y) in clicks { + let timer = cx + .background_executor() + .timer(Duration::from_millis(u64::from(click_ms))); + timer.await; + let position = gpui::point(px(x), px(y)); + log::info!("clicking at {x},{y}"); + let clicked = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, cx: &mut App| { + let arena_clear_needed = window.draw(cx); + arena_clear_needed.clear(cx); + let down = gpui::PlatformInput::MouseDown(gpui::MouseDownEvent { + button: gpui::MouseButton::Left, + position, + ..Default::default() + }); + window.dispatch_event(down, cx); + }, + ); + if let Err(error) = clicked { + log::error!("click at {x},{y} failed: {error}"); + } + if click_hold_ms > 0 { + cx.background_executor() + .timer(Duration::from_millis(u64::from(click_hold_ms))) + .await; + } + let _ = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, cx: &mut App| { + window.dispatch_event( + gpui::PlatformInput::MouseUp(gpui::MouseUpEvent { + button: gpui::MouseButton::Left, + position, + ..Default::default() + }), + cx, + ); + }, + ); + if !skip_synthetic_input_flush { + flush_js(&host, cx).await; + } + } + }) + .detach(); + } + + #[cfg(feature = "visual-capture")] + if !drags.is_empty() { + let host = host_for_input.clone(); + let frame_hook = frame_hook.clone(); + cx.spawn(async move |cx| { + for (index, (start_x, start_y, end_x, end_y, cancel)) in + drags.into_iter().enumerate() + { + let delay = if index == 0 { drag_start_ms } else { drag_ms }; + cx.background_executor() + .timer(Duration::from_millis(u64::from(delay))) + .await; + log::info!("dragging from {start_x},{start_y} to {end_x},{end_y}"); + let start = gpui::point(px(start_x), px(start_y)); + let down = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, cx: &mut App| { + let arena_clear_needed = window.draw(cx); + arena_clear_needed.clear(cx); + window.dispatch_event( + gpui::PlatformInput::MouseDown(gpui::MouseDownEvent { + button: gpui::MouseButton::Left, + position: start, + ..Default::default() + }), + cx, + ); + }, + ); + if let Err(error) = down { + log::error!("drag down failed: {error}"); + continue; + } + flush_js(&host, cx).await; + + for step in 1..=8 { + let amount = step as f32 / 8.0; + let position = gpui::point( + px(start_x + (end_x - start_x) * amount), + px(start_y + (end_y - start_y) * amount), + ); + let hook = frame_hook.clone(); + let moved = cx.update_window( + any_window, + move |_root: gpui::AnyView, + window: &mut gpui::Window, + cx: &mut App| { + let arena_clear_needed = window.draw(cx); + arena_clear_needed.clear(cx); + window.dispatch_event( + gpui::PlatformInput::MouseMove(gpui::MouseMoveEvent { + position, + pressed_button: Some(gpui::MouseButton::Left), + ..Default::default() + }), + cx, + ); + hook.arm(window); + }, + ); + if let Err(error) = moved { + log::error!("drag move {step} failed: {error}"); + break; + } + flush_js(&host, cx).await; + } + + let end = gpui::point(px(end_x), px(end_y)); + let hook = frame_hook.clone(); + let released = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, cx: &mut App| { + let arena_clear_needed = window.draw(cx); + arena_clear_needed.clear(cx); + if cancel { + crate::events::cancel_active_touch(window, cx, end); + } + window.dispatch_event( + gpui::PlatformInput::MouseUp(gpui::MouseUpEvent { + button: gpui::MouseButton::Left, + position: end, + ..Default::default() + }), + cx, + ); + hook.arm(window); + }, + ); + if let Err(error) = released { + log::error!("drag release failed: {error}"); + } + flush_js(&host, cx).await; + } + }) + .detach(); + } + + #[cfg(feature = "visual-capture")] + if !hovers.is_empty() { + let frame_hook = frame_hook.clone(); + let host = host_for_input.clone(); + cx.spawn(async move |cx| { + for (x, y) in hovers { + let timer = cx + .background_executor() + .timer(Duration::from_millis(u64::from(hover_ms))); + timer.await; + let position = gpui::point(px(x), px(y)); + log::info!("hovering at {x},{y}"); + let hook = frame_hook.clone(); + let hovered = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, cx: &mut App| { + // Hit testing uses the last drawn frame, and a + // headless run cannot rely on the platform asking + // for one after a mount batch, so draw first. + let arena_clear_needed = window.draw(cx); + arena_clear_needed.clear(cx); + let input = gpui::PlatformInput::MouseMove(gpui::MouseMoveEvent { + position, + pressed_button: None, + ..Default::default() + }); + window.dispatch_event(input, cx); + hook.arm(window); + }, + ); + if let Err(error) = hovered { + log::error!("hover at {x},{y} failed: {error}"); + } + flush_js(&host, cx).await; + } + }) + .detach(); + } + + #[cfg(feature = "visual-capture")] + if !scrolls.is_empty() { + let frame_hook = frame_hook.clone(); + let host = host_for_input.clone(); + cx.spawn(async move |cx| { + for (index, (x, y, dx, dy)) in scrolls.into_iter().enumerate() { + let delay = if index == 0 { scroll_start_ms } else { scroll_ms }; + cx.background_executor() + .timer(Duration::from_millis(u64::from(delay))) + .await; + let position = gpui::point(px(x), px(y)); + log::info!("scrolling at {x},{y} by {dx},{dy}"); + let hook = frame_hook.clone(); + let scrolled = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, cx: &mut App| { + let arena_clear_needed = window.draw(cx); + arena_clear_needed.clear(cx); + window.dispatch_event( + gpui::PlatformInput::ScrollWheel(gpui::ScrollWheelEvent { + position, + delta: gpui::ScrollDelta::Pixels(gpui::point(px(dx), px(dy))), + ..Default::default() + }), + cx, + ); + hook.arm(window); + }, + ); + if let Err(error) = scrolled { + log::error!("scroll at {x},{y} failed: {error}"); + } + flush_js(&host, cx).await; + } + }) + .detach(); + } + + #[cfg(feature = "visual-capture")] + if !keys.is_empty() { + let frame_hook = frame_hook.clone(); + let host = host_for_input.clone(); + cx.spawn(async move |cx| { + for key in keys { + cx.background_executor() + .timer(Duration::from_millis(u64::from(key_ms))) + .await; + let Ok(keystroke) = gpui::Keystroke::parse(&key) else { + log::warn!("invalid synthetic keystroke: {key}"); + continue; + }; + log::info!("pressing key {key}"); + let hook = frame_hook.clone(); + let pressed = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, cx: &mut App| { + let arena_clear_needed = window.draw(cx); + arena_clear_needed.clear(cx); + window.dispatch_keystroke(keystroke.clone(), cx); + window.dispatch_event( + gpui::PlatformInput::KeyUp(gpui::KeyUpEvent { keystroke }), + cx, + ); + hook.arm(window); + }, + ); + if let Err(error) = pressed { + log::error!("key {key} failed: {error}"); + } + flush_js(&host, cx).await; + } + }) + .detach(); + } + + #[cfg(feature = "visual-capture")] + if !mouse_navigation.is_empty() { + let host = host_for_input.clone(); + let frame_hook = frame_hook.clone(); + cx.spawn(async move |cx| { + for direction in mouse_navigation { + cx.background_executor() + .timer(Duration::from_millis(u64::from(mouse_navigation_ms))) + .await; + let button = match direction.as_str() { + "back" => gpui::MouseButton::Navigate(gpui::NavigationDirection::Back), + _ => gpui::MouseButton::Navigate(gpui::NavigationDirection::Forward), + }; + let hook = frame_hook.clone(); + let dispatched = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, cx: &mut App| { + let arena_clear_needed = window.draw(cx); + arena_clear_needed.clear(cx); + let position = gpui::point(px(1.), px(1.)); + window.dispatch_event( + gpui::PlatformInput::MouseDown(gpui::MouseDownEvent { + button, + position, + ..Default::default() + }), + cx, + ); + window.dispatch_event( + gpui::PlatformInput::MouseUp(gpui::MouseUpEvent { + button, + position, + ..Default::default() + }), + cx, + ); + hook.arm(window); + }, + ); + if let Err(error) = dispatched { + log::error!("synthetic mouse navigation {direction} failed: {error}"); + } else { + log::info!("pressed mouse navigation {direction}"); + } + flush_js(&host, cx).await; + } + }) + .detach(); + } + + #[cfg(all(target_os = "macos", feature = "visual-capture"))] + if env_u32("RN_GPUI_TITLEBAR_TOGGLE_MS", 0) > 0 { + let delay = env_u32("RN_GPUI_TITLEBAR_TOGGLE_MS", 0); + cx.spawn(async move |cx| { + for transparent in [false, true] { + cx.background_executor().timer(Duration::from_millis(u64::from(delay))).await; + let _ = cx.update_window(any_window, move |_, window, cx| { + crate::native_view::set_probe_titlebar_transparent(window, transparent); + window.bounds_changed(cx); + window.refresh(); + log::info!("[window-inset] probe titlebar transparent={transparent}"); + }); + } + }).detach(); + } + #[cfg(feature = "visual-capture")] + if env_u32("RN_GPUI_FULLSCREEN_MS", 0) > 0 { + let delay = env_u32("RN_GPUI_FULLSCREEN_MS", 0); + cx.spawn(async move |cx| { + for interval in [delay, 3000] { + cx.background_executor().timer(Duration::from_millis(u64::from(interval))).await; + let _ = cx.update_window(any_window, |_, window, _| window.toggle_fullscreen()); + } + }).detach(); + } + + // The subscription unsubscribes the moment it drops, so it is handed to the + // store. + let last_surface_size = Rc::new(Cell::new((width, height, scale, 0.0, 0.0))); + let bounds_subscription = window + .update(cx, |_root, window, cx| { + let store = cx.global::().0.clone(); + let frame_hook = frame_hook.clone(); + let window_state_path = window_state_path.clone(); + let last_surface_size = last_surface_size.clone(); + cx.observe_window_bounds(window, move |_root, window, cx| { + // GPUI calls bounds observers on every resize notification and layout, so + // nothing here may write to disk or log unless a value changed. + let size = window.viewport_size(); + let scale = scale_override.unwrap_or_else(|| window.scale_factor()); + let (width, height) = (f32::from(size.width), f32::from(size.height)); + let bounds = window.bounds(); + let origin = ( + f32::from(bounds.origin.x), + f32::from(bounds.origin.y), + ); + let previous = last_surface_size.get(); + let resized = (previous.0 - width).abs() > 0.01 + || (previous.1 - height).abs() > 0.01 + || (previous.2 - scale).abs() > 0.001; + let moved = (previous.3 - origin.0).abs() > 0.01 + || (previous.4 - origin.1).abs() > 0.01; + #[cfg(target_os = "macos")] + let top = crate::native_view::window_top_inset(window); + #[cfg(not(target_os = "macos"))] + let top = 0.0; + let inset_changed = cx + .try_global::() + .is_some_and(|global| (global.0.read(cx).top_inset - top).abs() > 0.01); + if !resized && !moved && !inset_changed { + return; + } + last_surface_size.set((width, height, scale, origin.0, origin.1)); + save_window_bounds(&window_state_path, bounds, size); + if inset_changed { + store.update(cx, |store, cx| { + store.top_inset = top; + store.host.set_window_top_inset(top); + store.frame_hook.arm(window); + for node in store.nodes.values() { + let component = node.read(cx).component.clone(); + let depends = crate::extensions::component_renderer(&component) + .is_some_and(|renderer| renderer.depends_on_window_insets()); + if depends { + node.update(cx, |_, cx| cx.notify()); + } + } + store.refresh_inherited(1, cx); + }); + log::info!( + "[window-inset] native window top inset={top} fullscreen={}", + window.is_fullscreen() + ); + } + if resized { + log::info!("window resized to {width}x{height}@{scale}x"); + let handle: gpui::AnyWindowHandle = window.window_handle().into(); + store.update(cx, |store, _cx| { + store.resize_window(handle, width, height, scale) + }); + frame_hook.arm(window); + } + }) + }) + .expect("failed to observe the window's bounds"); + // Windows only: its caption buttons and the title bar's double click maximize + // the window without the app being told, which this is how it learns. + let state_subscription = if cfg!(windows) { + Some( + window + .update(cx, |_root, window, cx| { + crate::desktop::windows::install_state_observer(window, cx) + }) + .expect("failed to observe the window's state"), + ) + } else { + None + }; + store.update(cx, |store, _cx| { + store.window_bounds_subscription = Some(bounds_subscription); + store.window_state_subscription = state_subscription; + }); + + #[cfg(feature = "visual-capture")] + if resize_ms > 0 { + let target = size(px(resize_width), px(resize_height)); + let timer = cx + .background_executor() + .timer(Duration::from_millis(u64::from(resize_ms))); + cx.spawn(async move |cx| { + timer.await; + log::info!("resizing the window to {resize_width}x{resize_height}"); + let resized = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, _cx: &mut App| { + window.resize(target) + }, + ); + if let Err(error) = resized { + log::error!("window resize failed: {error}"); + } + }) + .detach(); + } + + #[cfg(feature = "visual-capture")] + if run_ms > 0 { + let timer = cx + .background_executor() + .timer(Duration::from_millis(u64::from(run_ms))); + cx.spawn(async move |cx| { + timer.await; + log::info!("staying alive for {run_ms} ms — closing"); + cx.update(|cx| cx.quit()); + }) + .detach(); + } + + #[cfg(feature = "perf-timing")] + if benchmark_ms > 0 { + let timer = cx + .background_executor() + .timer(Duration::from_millis(u64::from(benchmark_ms))); + cx.spawn(async move |cx| { + timer.await; + let result = cx.update_window( + any_window, + move |_root: gpui::AnyView, window: &mut gpui::Window, _cx: &mut App| { + let snapshot = window.frame_duration_snapshot(); + let draw = &snapshot.draw_duration_histogram; + let interval = &snapshot.present_interval_histogram; + log::info!( + "[bench] mode={benchmark_mode} draw_frames={} draw_mean_ms={:.3} draw_p50_ms={:.3} draw_p95_ms={:.3} draw_p99_ms={:.3} draw_max_ms={:.3} present_intervals={} present_mean_ms={:.3} present_p95_ms={:.3}", + draw.len(), + draw.mean() / 1_000_000.0, + draw.value_at_quantile(0.50) as f64 / 1_000_000.0, + draw.value_at_quantile(0.95) as f64 / 1_000_000.0, + draw.value_at_quantile(0.99) as f64 / 1_000_000.0, + draw.max() as f64 / 1_000_000.0, + interval.len(), + interval.mean() / 1_000_000.0, + interval.value_at_quantile(0.95) as f64 / 1_000_000.0, + ); + }, + ); + if let Err(error) = result { + log::error!("benchmark snapshot failed: {error}"); + } + }) + .detach(); + } + + cx.spawn(async move |cx| { + let mut smoke_frame_armed = false; + #[cfg(feature = "visual-capture")] + for (delay_ms, path) in screenshots { + let store = store.clone(); + let mut path = Some(path); + cx.spawn(async move |cx| { + capture_and_quit(cx, &store, &mut path, delay_ms).await; + }) + .detach(); + } + #[cfg(feature = "visual-capture")] + if let Some(path) = screenshot { + let store = store.clone(); + let mut path = Some(path); + cx.spawn(async move |cx| { + capture_and_quit(cx, &store, &mut path, screenshot_delay_ms).await; + }) + .detach(); + } + while let Ok(event) = rx.recv().await { + match event { + MainEvent::Mount(batch) => { + let surface_id = batch.surface_id; + let started = std::time::Instant::now(); + store.update(cx, |store, cx| store.apply_batch(batch, cx)); + let elapsed = started.elapsed().as_secs_f32() * 1000.0; + cx.update(|cx| crate::desktop::perf_monitor::record_mount(cx, elapsed)); + // SAFETY: the C++ function decrements an atomic counter and takes no arguments. + unsafe { rn_gpui_mount_batch_applied() }; + // Smoke mode: an app-surface batch is React content, so + // the frame GPUI draws after it is the one a human sees. + if smoke && !smoke_frame_armed && surface_id == APP_SURFACE_ID { + smoke_frame_armed = true; + let window = store.read_with(cx, |store, _| store.window); + smoke_arm_first_frame(cx, window); + } + } + MainEvent::SurfaceStopped(surface_id) => { + if surface_id == rn_gpui_sys::ALL_SURFACES { + rn_gpui_sys::ffi::rn_gpui_rust_module_clear_pending(); + } + store.update(cx, |store, cx| store.surface_stopped(surface_id, cx)); + } + MainEvent::DevMenuShow => { + cx.update(|cx| crate::desktop::dev_menu::show(cx)); + } + MainEvent::TrayClick { + x, + y, + width, + height, + button, + } => { + cx.update(|cx| { + crate::desktop::tray::handle_click( + cx, + crate::desktop::tray::TrayRect { + x, + y, + width, + height, + }, + match button { + 1 => crate::desktop::tray::TrayButton::Right, + 2 => crate::desktop::tray::TrayButton::Middle, + _ => crate::desktop::tray::TrayButton::Left, + }, + ) + }); + } + MainEvent::MenuBarPerform(id) => { + cx.update(|cx| crate::desktop::app_menu::trigger_item(cx, &id)); + } + MainEvent::TrayMenuPerform(id) => { + cx.update(|cx| crate::desktop::tray::perform_menu(cx, &id)); + } + MainEvent::DevMenuPerform(id) => { + cx.update(|cx| crate::desktop::dev_menu::perform(cx, &id)); + } + MainEvent::DevSetting { name, enabled } => { + cx.update(|cx| { + crate::desktop::dev_menu::apply_setting(cx, &name, enabled) + }); + } + MainEvent::DesktopCommand(command) => { + cx.update(|cx| command(cx)); + } + MainEvent::MainTask(id) => { + rn_gpui_sys::ffi::rn_gpui_run_main_task(id); + } + MainEvent::RustJob(job) => { + cx.spawn(async move |_| { + job().await; + }) + .detach(); + } + MainEvent::AnimatedProps(props) => { + if props.has_opacity + || props.has_transform + || props.reset_transform + || props.has_background_color + || props.has_text_color + { + let mut target = None; + store.update(cx, |store, cx| { + target = store + .nodes + .get(&props.tag) + .cloned() + .map(|node| (node.read(cx).surface_id, node)); + }); + if let Some((surface_id, node)) = target { + node.update(cx, |node, cx| { + if props.has_opacity { + node.set_animated_opacity(props.opacity); + } + if props.has_background_color { + node.set_animated_background_color(props.background_color); + } + if props.has_text_color { + node.set_animated_text_color(props.text_color); + } + if props.has_transform { + node.set_animated_transform(Some(props.transform)); + } else if props.reset_transform { + node.set_animated_transform(None); + } + cx.notify(); + }); + if props.has_transform || props.reset_transform { + store.update(cx, |store, cx| { + store.refresh_inherited(surface_id, cx) + }); + } + } + } + } + MainEvent::AnimatedActivity(active) => { + let mut target = None; + store.update(cx, |store, _cx| { + target = Some((store.window, store.frame_hook.clone())); + }); + if let Some((window, frame_hook)) = target { + if let Err(error) = + cx.update_window(window, move |_root, window, cx| { + frame_hook.set_animation_active(active, window, cx); + }) + { + log::debug!( + "[native-animated] failed to update GPUI window activity: {error}" + ); + } + } else if std::env::var_os("RN_GPUI_DEBUG_ANIMATION").is_some() { + log::warn!("[native-animated] animation activity has no window"); + } + } + MainEvent::TextInputCommand { tag, focus } => { + let target = { + let mut target = None; + store.update(cx, |store, _cx| { + target = store.nodes.get(&tag).cloned().map(|node| (store.window, node)); + }); + target + }; + if let Some((window_handle, node)) = target { + let _ = cx.update_window(window_handle, move |_root, window, cx| { + node.update(cx, |node, cx| { + node.apply_text_input_focus_command(focus, window, cx); + }); + }); + } + } + MainEvent::AccessibilityFocus(tag) => { + let target = { + let mut target = None; + store.update(cx, |store, _cx| { + target = store + .nodes + .get(&tag) + .cloned() + .map(|node| (store.window, node)); + }); + target + }; + if let Some((window_handle, node)) = target { + let _ = cx.update_window(window_handle, move |_root, window, cx| { + node.update(cx, |node, cx| { + node.focus_for_accessibility(window, cx); + }); + }); + } + } + MainEvent::HostCommand { tag, command, args } => { + let target = { + let mut target = None; + store.update(cx, |store, _cx| { + target = store.nodes.get(&tag).cloned().map(|node| (store.window, node)); + }); + target + }; + if let Some((window_handle, node)) = target { + let _ = cx.update_window(window_handle, move |_root, _window, cx| { + node.update(cx, |node, cx| { + node.dispatch_host_command(command, &args, cx); + }); + }); + } + } + MainEvent::TextInputClear(tag) => { + let target = { + let mut target = None; + store.update(cx, |store, _cx| { + target = store.nodes.get(&tag).cloned().map(|node| (store.window, node)); + }); + target + }; + if let Some((window_handle, node)) = target { + let _ = cx.update_window(window_handle, move |_root, window, cx| { + node.update(cx, |node, cx| { + crate::builtins::textinput::input::clear_text_input(node, window, cx); + }); + }); + } + } + } + } + log::info!("mount channel closed"); + }) + .detach(); + + // A hidden test run must not take focus or pull the user to the + // desktop the app launched on. + #[cfg(feature = "test-driver")] + let background = env_bool("RN_GPUI_TEST_OFFSCREEN", false); + #[cfg(not(feature = "test-driver"))] + let background = false; + if background { + #[cfg(all(target_os = "macos", feature = "test-driver"))] + run_as_accessory_app(); + } else { + cx.activate(true); + } + log::info!("waiting for mount batches"); + }); +} + +#[cfg(feature = "visual-capture")] +async fn capture_and_quit( + cx: &mut gpui::AsyncApp, + store: &gpui::Entity, + path: &mut Option, + delay_ms: u32, +) { + let timer = cx + .background_executor() + .timer(Duration::from_millis(u64::from(delay_ms))); + timer.await; + + let Some(path) = path.take() else { return }; + let window = store.read_with(cx, |store, _| store.window); + let captured = cx.update_window( + window, + |_root: gpui::AnyView, window: &mut gpui::Window, cx: &mut App| { + // `render_to_image` only replays the last drawn scene, and a headless + // run can go idle after the first frame. Draw synchronously so the capture + // reflects the current element tree. + let arena_clear_needed = window.draw(cx); + arena_clear_needed.clear(cx); + window.render_to_image() + }, + ); + match captured { + Ok(Ok(image)) => match image.save(&path) { + Ok(()) => log::info!("screenshot saved to {path}"), + Err(error) => log::error!("could not save {path}: {error}"), + }, + Ok(Err(error)) => log::error!("render_to_image failed: {error}"), + Err(error) => log::error!("update_window failed: {error}"), + } + if env_bool("RN_GPUI_KEEP_ALIVE", false) { + log::info!("screenshot saved; keeping the GPUI window open"); + } else { + cx.update(|cx| cx.quit()); + } +} + +#[cfg(all(target_os = "macos", feature = "test-driver"))] +fn run_as_accessory_app() { + use objc::{class, msg_send, sel, sel_impl}; + const ACCESSORY: isize = 1; + // SAFETY: `NSApplication` is messaged from the main thread. + unsafe { + let app: cocoa::base::id = msg_send![class!(NSApplication), sharedApplication]; + let _: bool = msg_send![app, setActivationPolicy: ACCESSORY]; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_title_bar_name_maps_to_the_enum_the_schema_documents() { + assert_eq!( + WindowTitleBar::from_name("default"), + WindowTitleBar::Default + ); + assert_eq!( + WindowTitleBar::from_name("transparent"), + WindowTitleBar::Transparent + ); + assert_eq!(WindowTitleBar::from_name("hidden"), WindowTitleBar::Hidden); + } + + #[test] + fn an_unknown_title_bar_name_keeps_the_platform_bar() { + // The schema rejects a typo in `desktop.json`, but a secondary window's + // `titleBar` prop comes from JS and an older app can send anything. A + // window with the platform's own bar is the harmless outcome; a failed + // `open` would leave the app with no window at all. + assert_eq!(WindowTitleBar::from_name(""), WindowTitleBar::Default); + assert_eq!(WindowTitleBar::from_name("Hidden"), WindowTitleBar::Default); + assert_eq!( + WindowTitleBar::from_name("nonsense"), + WindowTitleBar::Default + ); + } + + #[test] + fn only_the_two_custom_kinds_are_custom() { + assert!(!WindowTitleBar::Default.is_custom()); + assert!(WindowTitleBar::Transparent.is_custom()); + assert!(WindowTitleBar::Hidden.is_custom()); + } + + #[test] + fn a_traffic_light_position_inside_the_window_is_unchanged() { + assert_eq!( + clamp_traffic_light_position(14.0, 10.0, 520.0, 360.0), + (14.0, 10.0) + ); + assert_eq!( + clamp_traffic_light_position(0.0, 0.0, 520.0, 360.0), + (0.0, 0.0) + ); + } + + #[test] + fn a_traffic_light_position_outside_the_window_is_pulled_back() { + // AppKit would move the buttons past the edge and leave no way to drag + // them back, so the host clamps instead. + assert_eq!( + clamp_traffic_light_position(900.0, 700.0, 520.0, 360.0), + (520.0, 360.0) + ); + assert_eq!( + clamp_traffic_light_position(-4.0, -8.0, 520.0, 360.0), + (0.0, 0.0) + ); + } + + #[test] + fn a_non_finite_or_degenerate_traffic_light_position_is_the_origin() { + // `f32::clamp` panics on NaN, and a window with no size has nowhere to + // put the buttons, so both answers are the origin rather than a panic. + assert_eq!( + clamp_traffic_light_position(f32::NAN, f32::INFINITY, 520.0, 360.0), + (0.0, 0.0) + ); + assert_eq!( + clamp_traffic_light_position(10.0, 10.0, 0.0, 0.0), + (0.0, 0.0) + ); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/alert/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/alert/mod.rs new file mode 100644 index 0000000000..74494a750c --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/alert/mod.rs @@ -0,0 +1,89 @@ +use gpui::{App, AppContext}; +use rn_gpui_module::{ModuleThread, Value}; + +use crate::builtins::rn_gpui_specs::{ + AlertArgs, NativeGpuiAlertManagerEvents, NativeGpuiAlertManagerSpec, +}; + +struct AlertManagerModule; + +fn button_labels(buttons: Option>) -> Vec { + let Some(buttons) = buttons else { + return Vec::new(); + }; + let mut labels: Vec<(usize, String)> = buttons + .into_iter() + .enumerate() + .filter_map(|(position, button)| match button { + Value::String(text) => Some((position, text)), + Value::Object(fields) => fields.iter().find_map(|(key, value)| match value { + Value::String(text) => Some((key.parse().unwrap_or(position), text.clone())), + _ => None, + }), + _ => None, + }) + .collect(); + labels.sort_by_key(|(position, _)| *position); + labels.into_iter().map(|(_, text)| text).collect() +} + +impl NativeGpuiAlertManagerSpec for AlertManagerModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiAlertManagerEvents) -> Self { + Self + } + + fn alert_with_args(&self, args: AlertArgs, callback: Box) { + let title = args.title.unwrap_or_default(); + let message = args.message.unwrap_or_default(); + let mut buttons = button_labels(args.buttons); + if buttons.is_empty() { + buttons.push("OK".to_string()); + } + let answers: Vec = buttons + .iter() + .map(|button| gpui::PromptButton::new(button.clone())) + .collect(); + crate::desktop::post_to_main(Box::new(move |cx: &mut App| { + let store = match cx.try_global::() { + Some(store) => store.0.clone(), + None => { + callback(0.0, String::new()); + return; + } + }; + let mut window = None; + store.update(cx, |store, _cx| window = Some(store.window)); + let Some(window) = window else { + callback(0.0, String::new()); + return; + }; + let receiver = cx.update_window(window, move |_root, window, cx| { + window.prompt( + gpui::PromptLevel::Info, + &title, + (!message.is_empty()).then_some(message.as_str()), + &answers, + cx, + ) + }); + match receiver { + Ok(receiver) => { + cx.spawn(async move |_cx| { + let index = receiver.await.unwrap_or(0); + callback(index as f64, String::new()); + }) + .detach(); + } + Err(_) => callback(0.0, String::new()), + } + })); + } +} + +pub fn register() { + rn_gpui_module::register( + crate::builtins::rn_gpui_specs::native_gpui_alert_manager_module::(), + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/app_menu/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/app_menu/mod.rs new file mode 100644 index 0000000000..31cc682727 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/app_menu/mod.rs @@ -0,0 +1,146 @@ +use rn_gpui_module::{FromValue, IntoValue, ModuleError, ModuleThread, Path, Value}; + +use crate::builtins::main_thread::on_main; +use crate::builtins::rn_gpui_specs::{ + MenuDescription, MenuItemPatch, NativeGpuiAppMenuEvents, NativeGpuiAppMenuSpec, +}; +use crate::desktop::app_menu; +use crate::desktop::errors::{DesktopError, DesktopResult}; +use crate::desktop::windows::MAIN_WINDOW_KEY; + +const MODULE: &str = "GpuiAppMenu"; + +struct AppMenuModule; + +fn value_json(value: Value) -> serde_json::Value { + match value { + Value::Null | Value::Callback(_) => serde_json::Value::Null, + Value::Bool(flag) => serde_json::Value::Bool(flag), + Value::Number(number) => serde_json::Number::from_f64(number) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + Value::String(text) => serde_json::Value::String(text), + Value::Array(items) => { + serde_json::Value::Array(items.into_iter().map(value_json).collect()) + } + Value::Object(fields) => serde_json::Value::Object( + fields + .into_iter() + .map(|(key, item)| (key, value_json(item))) + .collect(), + ), + Value::Buffer(bytes) => serde_json::Value::Array( + bytes + .into_iter() + .map(|byte| serde_json::Value::Number(byte.into())) + .collect(), + ), + } +} + +fn json_value(value: &serde_json::Value) -> Value { + match value { + serde_json::Value::Null => Value::Null, + serde_json::Value::Bool(flag) => Value::Bool(*flag), + serde_json::Value::Number(number) => { + number.as_f64().map(Value::Number).unwrap_or(Value::Null) + } + serde_json::Value::String(text) => Value::String(text.clone()), + serde_json::Value::Array(items) => Value::Array(items.iter().map(json_value).collect()), + serde_json::Value::Object(fields) => Value::Object( + fields + .iter() + .map(|(key, item)| (key.clone(), json_value(item))) + .collect(), + ), + } +} + +fn json_items(items: Option>) -> Option { + items.map(|items| serde_json::Value::Array(items.into_iter().map(value_json).collect())) +} + +fn module_error(error: DesktopError) -> ModuleError { + ModuleError::new(error.message) +} + +fn typed(method: &'static str, result: DesktopResult) -> Result { + let mut path = Path::result(MODULE, method); + let value = json_value(&result.map_err(module_error)?); + T::from_value(value, &mut path).map_err(|error| ModuleError::new(error.message)) +} + +impl NativeGpuiAppMenuSpec for AppMenuModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiAppMenuEvents) -> Self { + Self + } + + async fn menu_set_application_menu( + &self, + items: Option>, + ) -> Result<(), ModuleError> { + let items = json_items(items); + if let Some(items) = &items { + crate::desktop::icon::preload_menu(items).await; + } + on_main(move |cx| { + app_menu::set_application_menu(cx, items.as_ref())?; + Ok(serde_json::Value::Null) + }) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn menu_update(&self, id: String, patch: MenuItemPatch) -> Result<(), ModuleError> { + on_main(move |cx| { + app_menu::update_item(cx, &id, &value_json(patch.into_value()))?; + Ok(serde_json::Value::Null) + }) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn menu_set_menu_bar_visible( + &self, + visible: bool, + window_id: Option, + ) -> Result<(), ModuleError> { + let window_id = window_id.map(|id| id as u32).unwrap_or(MAIN_WINDOW_KEY); + on_main(move |cx| { + app_menu::set_menu_bar_visible(cx, window_id, visible)?; + Ok(serde_json::Value::Null) + }) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn menu_perform_role(&self, role: String) -> Result<(), ModuleError> { + on_main(move |cx| { + app_menu::perform_role(cx, &role)?; + Ok(serde_json::Value::Null) + }) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn menu_get(&self) -> Result { + let result = on_main(|cx| -> DesktopResult { Ok(app_menu::menu_json(cx)) }).await?; + typed("menuGet", result) + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +pub fn register() { + rn_gpui_module::register( + crate::builtins::rn_gpui_specs::native_gpui_app_menu_module::(), + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/clipboard/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/clipboard/mod.rs new file mode 100644 index 0000000000..114dd7873c --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/clipboard/mod.rs @@ -0,0 +1,41 @@ +use rn_gpui_module::{ModuleError, ModuleThread}; + +use crate::builtins::main_thread::on_main; +use crate::builtins::rn_gpui_specs::{ + GetConstantsResult, NativeGpuiClipboardEvents, NativeGpuiClipboardSpec, +}; + +struct ClipboardModule; + +impl NativeGpuiClipboardSpec for ClipboardModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiClipboardEvents) -> Self { + Self + } + + fn get_constants(&self) -> Result { + Ok(GetConstantsResult {}) + } + + async fn get_string(&self) -> Result { + on_main(|cx| { + cx.read_from_clipboard() + .and_then(|item| item.text()) + .unwrap_or_default() + }) + .await + } + + fn set_string(&self, content: String) { + crate::desktop::post_to_main(Box::new(move |cx| { + cx.write_to_clipboard(gpui::ClipboardItem::new_string(content)); + })); + } +} + +pub fn register() { + rn_gpui_module::register( + crate::builtins::rn_gpui_specs::native_gpui_clipboard_module::(), + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/context_menu/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/context_menu/mod.rs new file mode 100644 index 0000000000..c6e2af9b07 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/context_menu/mod.rs @@ -0,0 +1,113 @@ +use futures::channel::oneshot; +use gpui::App; +use rn_gpui_module::{FromValue, ModuleError, ModuleThread, Path, Value}; + +use crate::builtins::rn_gpui_specs::{ + ContextMenuOptions, ContextMenuSelection, NativeGpuiContextMenuEvents, + NativeGpuiContextMenuSpec, +}; +use crate::desktop::context_menu; +use crate::desktop::errors::DesktopError; + +const MODULE: &str = "GpuiContextMenu"; + +struct ContextMenuModule; + +fn window_key(window_id: i32) -> u32 { + window_id as u32 +} + +fn to_json(value: Value) -> serde_json::Value { + match value { + Value::Null | Value::Callback(_) => serde_json::Value::Null, + Value::Bool(flag) => serde_json::Value::Bool(flag), + Value::Number(number) => serde_json::Number::from_f64(number) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + Value::String(text) => serde_json::Value::String(text), + Value::Array(items) => serde_json::Value::Array(items.into_iter().map(to_json).collect()), + Value::Object(fields) => serde_json::Value::Object( + fields + .into_iter() + .map(|(key, item)| (key, to_json(item))) + .collect(), + ), + Value::Buffer(bytes) => serde_json::Value::Array( + bytes + .into_iter() + .map(|byte| serde_json::Value::Number(byte.into())) + .collect(), + ), + } +} + +fn from_json(value: &serde_json::Value) -> Value { + match value { + serde_json::Value::Null => Value::Null, + serde_json::Value::Bool(flag) => Value::Bool(*flag), + serde_json::Value::Number(number) => { + number.as_f64().map(Value::Number).unwrap_or(Value::Null) + } + serde_json::Value::String(text) => Value::String(text.clone()), + serde_json::Value::Array(items) => Value::Array(items.iter().map(from_json).collect()), + serde_json::Value::Object(fields) => Value::Object( + fields + .iter() + .map(|(key, item)| (key.clone(), from_json(item))) + .collect(), + ), + } +} + +fn module_error(error: DesktopError) -> ModuleError { + ModuleError::new(error.message) +} + +impl NativeGpuiContextMenuSpec for ContextMenuModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiContextMenuEvents) -> Self { + Self + } + + async fn context_menu_show( + &self, + items: Vec, + options: ContextMenuOptions, + ) -> Result { + let items = serde_json::Value::Array(items.into_iter().map(to_json).collect()); + crate::desktop::icon::preload_menu(&items).await; + let (sender, receiver) = oneshot::channel(); + crate::desktop::post_to_main(Box::new(move |cx: &mut App| { + let responder = context_menu::Responder::desktop(sender); + let rejected = responder.clone(); + if let Err(error) = context_menu::show( + cx, + items, + options.x, + options.y, + options.window_id.map(window_key), + responder, + ) { + rejected.reject(error); + } + })); + let result = receiver + .await + .map_err(|_| ModuleError::new("the command was dismissed"))? + .map_err(module_error)?; + let mut path = Path::result(MODULE, "contextMenuShow"); + ContextMenuSelection::from_value(from_json(&result), &mut path) + .map_err(|error| ModuleError::new(error.message)) + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +pub fn register() { + rn_gpui_module::register( + crate::builtins::rn_gpui_specs::native_gpui_context_menu_module::(), + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/context_menu_view/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/context_menu_view/mod.rs new file mode 100644 index 0000000000..aaab52be43 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/context_menu_view/mod.rs @@ -0,0 +1,285 @@ +//! A view whose context menu opens natively. +//! +//! A right click (or Ctrl + click on macOS, or the Menu key on Windows and +//! Linux) opens the view's `items` straight from native, so the menu appears even +//! while JS is busy. JS only hears about the item that was chosen, through +//! `onMenuSelect`; the menu itself was never described to JS at the moment it +//! opened. +//! +//! The component lays out like a `View` — core paints the element, the style and +//! the children — so this renderer only claims the press and presents the menu. + +use std::sync::Arc; + +use gpui::{AnyElement, App, Window}; +use serde_json::Value; + +use crate::desktop::menu_model::{self, MenuModel}; +use crate::events::ContextMenuTriggerSource; +use crate::extensions::input::{ContextMenuTrigger, InputContext, InputSpec, Propagation}; +use crate::extensions::{register_component_renderer, ComponentNode, ComponentRenderer}; + +const OPEN_EVENT: &str = "topMenuOpen"; + +/// What a press on the view should do, decided from the parsed props alone. +/// +/// Split out so the three outcomes — show a menu, stay out of the way, or report +/// a model that cannot be read — are testable without a window or a presenter. +enum MenuRequest { + /// No `items`, or the press is not this view's to take. + None, + Menu(MenuModel), + /// `items` was supposed to be a menu and is not one. + Invalid(crate::desktop::errors::DesktopError), +} + +/// Whether the press on this view belongs to its menu. +/// +/// A disabled view has nothing to open, and a `custom` presentation is drawn by +/// JS, so neither takes the press. +fn claims_press(props: &Value) -> bool { + if props + .get("disabled") + .and_then(Value::as_bool) + .unwrap_or(false) + { + return false; + } + // `custom` is drawn by JS, so the descending `View` opens it through + // `onContextMenu`, exactly as it did before this component existed. Claiming + // the press here would open a native menu over the React one. + match props.get("presentation").and_then(Value::as_str) { + Some("custom") => false, + _ => true, + } +} + +/// Whether the props carry something that could be a menu. +/// +/// An empty array is not one: `items` defaults to `[]` on the C++ side, and a +/// caller that filtered a dynamic menu down to nothing has no menu to show. Either +/// way the press belongs to whatever is behind the view, so this is not a menu +/// owner at all rather than an owner that shows nothing. +fn has_items(props: &Value) -> bool { + match props.get("items") { + None | Some(Value::Null) => false, + Some(Value::Array(items)) => !items.is_empty(), + // Not an array is a mistake worth reporting, so it still counts as "the + // caller meant to pass a menu"; `request` turns it into `Invalid`. + Some(_) => true, + } +} + +fn request(props: &Value) -> MenuRequest { + if !claims_press(props) || !has_items(props) { + return MenuRequest::None; + } + match menu_model::parse_menu(&props["items"]) { + Ok(model) => MenuRequest::Menu(model), + Err(error) => MenuRequest::Invalid(error), + } +} + +fn trigger_name(source: ContextMenuTriggerSource) -> &'static str { + match source { + ContextMenuTriggerSource::Mouse => "mouse", + ContextMenuTriggerSource::Keyboard => "keyboard", + } +} + +/// The `topMenuOpen` payload, in the shape `topContextMenu` uses. +/// +/// The two are the same press reported through different components, so a +/// handler written for `onContextMenu` keeps working when the wrapper switches +/// to the native path. +fn open_payload(trigger: &ContextMenuTrigger, window_id: u32) -> serde_json::Value { + let x = f32::from(trigger.position.x); + let y = f32::from(trigger.position.y); + serde_json::json!({ + "x": x, + "y": y, + // The menu is placed in window coordinates, which is also the page's + // here: the surface fills the window, and the top inset is part of the + // layer's own origin rather than something a handler would add back. + "pageX": x, + "pageY": y, + "windowId": window_id, + "button": trigger.button.number(), + "ctrlKey": trigger.modifiers.ctrl, + "shiftKey": trigger.modifiers.shift, + "altKey": trigger.modifiers.alt, + "metaKey": trigger.modifiers.meta, + "trigger": trigger_name(trigger.source), + }) +} + +struct ContextMenuViewRenderer; + +impl ComponentRenderer for ContextMenuViewRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn input(&self, node: &ComponentNode) -> InputSpec { + if !claims_press(&node.props) || !has_items(&node.props) { + return InputSpec::default(); + } + crate::desktop::icon::prefetch_menu(&node.props["items"]); + InputSpec { + // No buttons: the right press is already reported by the node's own + // listener. This flag is what makes the press find this component as + // the menu's owner, and what keeps a parent `View`'s `onContextMenu` + // from also firing. + context_menu: true, + ..InputSpec::default() + } + } + + fn on_context_menu( + &self, + node: &ComponentNode, + trigger: &ContextMenuTrigger, + cx: &mut InputContext<'_>, + ) -> Propagation { + match request(&node.props) { + MenuRequest::None => Propagation::Continue, + MenuRequest::Invalid(error) => { + // A model JS built wrong is not a reason to swallow the press: an + // ancestor's menu can still be a real one. + log::warn!("[context-menu-view] ignoring unreadable items: {error}"); + Propagation::Continue + } + MenuRequest::Menu(model) => { + cx.show_context_menu(model, trigger.position); + // After the menu is up, so a handler that reads its own state + // sees it already open. The payload carries what `onContextMenu` + // reports, because the component keeps that prop working on this + // path and the native side is the only place these fields exist. + cx.dispatch_event(OPEN_EVENT, &open_payload(trigger, cx.window_id())); + // Only the innermost view opens, as native menus do. + Propagation::Stop + } + } + } +} + +pub fn register() { + register_component_renderer("DesktopContextMenuView", Arc::new(ContextMenuViewRenderer)); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::desktop::menu_model::MenuNode; + use serde_json::json; + + fn props(value: Value) -> Arc { + Arc::new(value) + } + + fn node(value: Value) -> ComponentNode { + ComponentNode { + component: "DesktopContextMenuView".to_string(), + tag: 9, + frame: rn_gpui_sys::ffi::RectF { + x: 0.0, + y: 0.0, + w: 200.0, + h: 40.0, + }, + scale: (1.0, 1.0), + props: props(value), + parent: None, + children: Vec::new(), + } + } + + const ITEMS: &str = r#"[ + {"id": "open", "label": "Open"}, + {"type": "submenu", "label": "More", "items": [ + {"id": "nested", "label": "Nested"} + ]}, + {"type": "separator"}, + {"id": "pin", "label": "Pin", "checked": true} + ]"#; + + fn items() -> Value { + serde_json::from_str(ITEMS).expect("valid json") + } + + #[test] + fn items_become_the_same_model_a_js_menu_uses() { + let value = json!({"items": items()}); + let MenuRequest::Menu(model) = request(&value) else { + panic!("a readable menu"); + }; + assert_eq!(model.ids(), vec!["open", "nested", "pin"]); + assert!(matches!(model.items[1], MenuNode::Submenu(_))); + assert!(matches!(model.items[2], MenuNode::Separator)); + let MenuNode::Item(pin) = &model.items[3] else { + panic!("a leaf item"); + }; + assert!(pin.checked); + } + + #[test] + fn a_disabled_view_asks_for_nothing() { + let value = json!({"items": items(), "disabled": true}); + assert!(!claims_press(&value)); + assert!(matches!(request(&value), MenuRequest::None)); + // The spec is the other half: a disabled view must not become a menu + // owner, or the press would be claimed with nothing to show. + let spec = ContextMenuViewRenderer.input(&node(value)); + assert!(!spec.context_menu); + assert_eq!(spec, InputSpec::default()); + } + + #[test] + fn a_custom_presentation_is_left_to_js() { + let value = json!({"items": items(), "presentation": "custom"}); + assert!(!claims_press(&value)); + assert!(!ContextMenuViewRenderer.input(&node(value)).context_menu); + } + + #[test] + fn native_and_default_presentations_open_here() { + for presentation in [json!("default"), json!("native"), json!(null)] { + let value = json!({"items": items(), "presentation": presentation}); + assert!(claims_press(&value), "{presentation}"); + assert!(ContextMenuViewRenderer.input(&node(value)).context_menu); + } + } + + #[test] + fn items_that_are_not_a_menu_are_reported_rather_than_swallowed() { + // The shape a caller gets wrong most easily: an item with no label. + let value = json!({"items": [{"id": "a"}]}); + let MenuRequest::Invalid(error) = request(&value) else { + panic!("an unreadable model"); + }; + assert_eq!(error.code, crate::desktop::errors::codes::INVALID_ARGUMENT); + // Still an owner, so the hook runs and can log; the press is then passed + // on rather than stopped. + assert!(ContextMenuViewRenderer.input(&node(value)).context_menu); + } + + #[test] + fn no_items_is_simply_no_menu() { + assert!(matches!(request(&json!({})), MenuRequest::None)); + } + + #[test] + fn an_empty_menu_is_not_a_menu() { + // What the C++ side sends for `items` left out, and what a caller that + // filtered every entry out of a dynamic menu sends. Neither should claim + // the press and then show nothing. + assert!(matches!(request(&json!({"items": []})), MenuRequest::None)); + let spec = ContextMenuViewRenderer.input(&node(json!({"items": []}))); + assert!(!spec.context_menu); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/dev_menu/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/dev_menu/mod.rs new file mode 100644 index 0000000000..18eae96190 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/dev_menu/mod.rs @@ -0,0 +1,53 @@ +use rn_gpui_module::ModuleThread; + +use crate::builtins::rn_gpui_specs::{NativeGpuiDevMenuEvents, NativeGpuiDevMenuSpec}; + +struct DevMenuModule; + +impl NativeGpuiDevMenuSpec for DevMenuModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiDevMenuEvents) -> Self { + Self + } + + fn show(&self) { + if let Some(handler) = rn_gpui_sys::DEV_MENU_SHOW_HANDLER.get() { + handler(); + } + } + + fn reload(&self) { + if let Some(handler) = rn_gpui_sys::DEV_MENU_PERFORM_HANDLER.get() { + handler(crate::desktop::dev_menu::RELOAD.to_string()); + } + } + + fn set_profiling_enabled(&self, enabled: bool) { + dev_setting("profiling", enabled); + } + + fn set_hot_loading_enabled(&self, enabled: bool) { + dev_setting("hotLoading", enabled); + } + + fn toggle_element_inspector(&self) { + dev_setting("inspectorToggle", true); + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +fn dev_setting(name: &str, enabled: bool) { + if let Some(handler) = rn_gpui_sys::DEV_SETTING_HANDLER.get() { + handler(name.to_string(), enabled); + } +} + +pub fn register() { + rn_gpui_module::register( + crate::builtins::rn_gpui_specs::native_gpui_dev_menu_module::(), + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/image/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/image/mod.rs new file mode 100644 index 0000000000..0f1e0fa016 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/image/mod.rs @@ -0,0 +1,52 @@ +use gpui::{prelude::*, AnyElement, App, ObjectFit, Window}; +use serde_json::Value; + +use crate::extensions::{register_component_renderer, ComponentNode, ComponentRenderer}; + +struct ImageRenderer; + +impl ComponentRenderer for ImageRenderer { + fn content( + &self, + node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + paint( + node.props.get("uri")?.as_str()?, + node.props.get("resizeMode"), + ) + } + + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } +} + +fn paint(uri: &str, resize_mode: Option<&Value>) -> Option { + let fit = match resize_mode.and_then(Value::as_str) { + Some("contain") => ObjectFit::Contain, + Some("cover") => ObjectFit::Cover, + Some("center") => ObjectFit::ScaleDown, + _ => ObjectFit::Fill, + }; + let source = if let Some(data) = uri.strip_prefix("data:") { + crate::node_view::decode_data_image(data).map(|image| gpui::img(std::sync::Arc::new(image))) + } else if let Some(path) = crate::image_loader::file_uri_path(uri) { + Some(gpui::img(path)) + } else { + Some(gpui::img( + crate::image_loader::dev_server_asset_url(uri).into_owned(), + )) + }; + Some(source?.size_full().object_fit(fit).into_any_element()) +} + +pub fn register() { + register_component_renderer("Image", std::sync::Arc::new(ImageRenderer)); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/linking/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/linking/mod.rs new file mode 100644 index 0000000000..10ea65a0f7 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/linking/mod.rs @@ -0,0 +1,52 @@ +use rn_gpui_module::{ModuleError, ModuleThread}; + +use crate::builtins::main_thread::on_main; +use crate::builtins::rn_gpui_specs::{ + NativeGpuiLinkingManagerEvents, NativeGpuiLinkingManagerSpec, +}; + +struct LinkingManagerModule; + +impl NativeGpuiLinkingManagerSpec for LinkingManagerModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiLinkingManagerEvents) -> Self { + Self + } + + async fn get_initial_url(&self) -> Result, ModuleError> { + Ok(None) + } + + async fn can_open_url(&self, url: String) -> Result { + let scheme = url + .split_once(':') + .map(|(scheme, _)| scheme.to_ascii_lowercase()) + .unwrap_or_default(); + Ok(matches!( + scheme.as_str(), + "http" | "https" | "mailto" | "tel" + )) + } + + async fn open_url(&self, url: String) -> Result<(), ModuleError> { + on_main(move |cx| cx.open_url(&url)).await + } + + async fn open_settings(&self) -> Result<(), ModuleError> { + Err(ModuleError::new( + "opening application settings is not supported by GPUI", + )) + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +pub fn register() { + rn_gpui_module::register( + crate::builtins::rn_gpui_specs::native_gpui_linking_manager_module::( + ), + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/main_thread.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/main_thread.rs new file mode 100644 index 0000000000..d1fec882d0 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/main_thread.rs @@ -0,0 +1,15 @@ +use futures::channel::oneshot; +use gpui::App; +use rn_gpui_module::ModuleError; + +pub(crate) async fn on_main( + command: impl FnOnce(&mut App) -> T + Send + 'static, +) -> Result { + let (sender, receiver) = oneshot::channel(); + crate::desktop::post_to_main(Box::new(move |cx| { + let _ = sender.send(command(cx)); + })); + receiver + .await + .map_err(|_| ModuleError::new("the desktop shell is not running")) +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/mod.rs new file mode 100644 index 0000000000..8db9399019 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/mod.rs @@ -0,0 +1,39 @@ +rn_gpui_module::include_specs!(); + +pub mod alert; +pub mod app_menu; +pub mod clipboard; +pub mod context_menu; +pub mod context_menu_view; +pub mod dev_menu; +pub mod image; +pub mod linking; +pub mod main_thread; +pub mod modal; +pub mod perf_monitor; +pub mod scrollview; +pub mod shortcut_scope; +pub mod shortcuts; +pub mod textinput; +pub mod tray; +pub mod window_control; +pub mod windows; + +pub fn register() { + alert::register(); + app_menu::register(); + clipboard::register(); + context_menu::register(); + context_menu_view::register(); + dev_menu::register(); + image::register(); + linking::register(); + modal::register(); + perf_monitor::register(); + scrollview::register(); + shortcut_scope::register(); + shortcuts::register(); + tray::register(); + window_control::register(); + windows::register(); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/modal/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/modal/mod.rs new file mode 100644 index 0000000000..ef9d78f34a --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/modal/mod.rs @@ -0,0 +1,33 @@ +use serde_json::Value; + +use crate::extensions::{register_component_renderer, ComponentNode, ComponentRenderer, Portal}; +use gpui::{AnyElement, App, Window}; + +const CLOSE_REQUEST_EVENT: &str = "topRequestClose"; + +struct ModalRenderer; + +impl ComponentRenderer for ModalRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn portal(&self, props: &Value) -> Option { + Some(Portal { + visible: props + .get("visible") + .and_then(Value::as_bool) + .unwrap_or(true), + close_request_event: Some(CLOSE_REQUEST_EVENT.to_string()), + }) + } +} + +pub fn register() { + register_component_renderer("ModalHostView", std::sync::Arc::new(ModalRenderer)); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/perf_monitor/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/perf_monitor/mod.rs new file mode 100644 index 0000000000..44b15b157a --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/perf_monitor/mod.rs @@ -0,0 +1,63 @@ +use rn_gpui_module::{FromValue, ModuleError, ModuleThread, Path, Value}; + +use crate::builtins::main_thread::on_main; +use crate::builtins::rn_gpui_specs::{ + NativeGpuiPerfMonitorEvents, NativeGpuiPerfMonitorSpec, PerfMetrics, PerfToggle, +}; + +const MODULE: &str = "GpuiPerfMonitor"; + +struct PerfMonitorModule; + +fn typed(method: &'static str, value: &serde_json::Value) -> Result { + let mut path = Path::result(MODULE, method); + let value = json_value(value); + T::from_value(value, &mut path).map_err(|error| ModuleError::new(error.message)) +} + +fn json_value(value: &serde_json::Value) -> Value { + match value { + serde_json::Value::Null => Value::Null, + serde_json::Value::Bool(flag) => Value::Bool(*flag), + serde_json::Value::Number(number) => { + number.as_f64().map(Value::Number).unwrap_or(Value::Null) + } + serde_json::Value::String(text) => Value::String(text.clone()), + serde_json::Value::Array(items) => Value::Array(items.iter().map(json_value).collect()), + serde_json::Value::Object(fields) => Value::Object( + fields + .iter() + .map(|(key, item)| (key.clone(), json_value(item))) + .collect(), + ), + } +} + +impl NativeGpuiPerfMonitorSpec for PerfMonitorModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiPerfMonitorEvents) -> Self { + Self + } + + async fn perf_monitor_set_enabled(&self, enabled: bool) -> Result { + let value = + on_main(move |cx| crate::desktop::perf_monitor::set_enabled(cx, enabled)).await?; + typed("perfMonitorSetEnabled", &value) + } + + async fn perf_monitor_get_metrics(&self) -> Result { + let value = on_main(|cx| crate::desktop::perf_monitor::metrics(cx)).await?; + typed("perfMonitorGetMetrics", &value) + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +pub fn register() { + rn_gpui_module::register( + crate::builtins::rn_gpui_specs::native_gpui_perf_monitor_module::(), + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/scrollview/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/scrollview/mod.rs new file mode 100644 index 0000000000..39df2079f9 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/scrollview/mod.rs @@ -0,0 +1,46 @@ +use serde_json::Value; + +use crate::extensions::{ + register_component_renderer, ComponentNode, ComponentRenderer, ScrollSpec, +}; +use gpui::{AnyElement, App, Window}; + +const SCROLL_EVENT: &str = "topScroll"; + +struct ScrollViewRenderer; + +impl ComponentRenderer for ScrollViewRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn scroll(&self, props: &Value) -> Option { + let number = |key: &str| props.get(key).and_then(Value::as_f64).unwrap_or(0.0) as f32; + Some(ScrollSpec { + content_size: (number("contentWidth"), number("contentHeight")), + horizontal: props + .get("horizontal") + .and_then(Value::as_bool) + .unwrap_or(false), + enabled: props + .get("enabled") + .and_then(Value::as_bool) + .unwrap_or(true), + indicator: props + .get("indicator") + .and_then(Value::as_bool) + .unwrap_or(true), + offset: (number("offsetX"), number("offsetY")), + event: Some(SCROLL_EVENT.to_string()), + }) + } +} + +pub fn register() { + register_component_renderer("ScrollView", std::sync::Arc::new(ScrollViewRenderer)); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/shortcut_scope/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/shortcut_scope/mod.rs new file mode 100644 index 0000000000..9bbba2cea4 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/shortcut_scope/mod.rs @@ -0,0 +1,115 @@ +//! A named scope for keyboard shortcuts. +//! +//! The component paints nothing of its own: core builds the box, the style and +//! the children exactly as it does for a `View`, and this only adds the node's +//! key context. Scopes nest, and GPUI resolves a keystroke against the contexts +//! on the focused element's dispatch path — so a binding in an inner scope +//! outranks one in an outer scope without any of that logic living here. +//! +//! The action listener lives in `node_view.rs` beside the element, because a +//! `ComponentRenderer` never sees the element it would have to attach to. + +use std::sync::Arc; + +use gpui::{AnyElement, App, Window}; +use serde_json::Value; + +use crate::desktop::keymap; +use crate::extensions::{register_component_renderer, ComponentNode, ComponentRenderer}; + +pub const COMPONENT: &str = "DesktopShortcutScope"; + +/// The scope a `ShortcutScope` node declares, or an empty string when its props +/// carry no usable name. +/// +/// A scope with no name claims nothing: it still groups its children, but no +/// shortcut can be registered into it, which is better than every unnamed scope +/// sharing one context and overriding the others. +pub fn scope_name(node: &ComponentNode) -> String { + node.props + .get("name") + .and_then(Value::as_str) + .unwrap_or_default() + .to_string() +} + +struct ShortcutScopeRenderer; + +impl ComponentRenderer for ShortcutScopeRenderer { + /// Core paints the element, so the renderer claims no appearance at all. + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + None + } + + fn key_context(&self, node: &ComponentNode) -> Option { + let name = scope_name(node); + if name.is_empty() { + return None; + } + Some(keymap::scope_context(&name)) + } +} + +pub fn register() { + register_component_renderer("DesktopShortcutScope", Arc::new(ShortcutScopeRenderer)); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + fn node(props: Value) -> ComponentNode { + ComponentNode { + component: COMPONENT.to_string(), + tag: 1, + frame: rn_gpui_sys::ffi::RectF { + x: 0.0, + y: 0.0, + w: 100.0, + h: 100.0, + }, + scale: (1.0, 1.0), + props: Arc::new(props), + parent: None, + children: Vec::new(), + } + } + + #[test] + fn a_named_scope_gets_its_context() { + let renderer = ShortcutScopeRenderer; + assert_eq!( + renderer.key_context(&node(serde_json::json!({"name": "editor"}))), + Some("rn-scope-editor".to_string()) + ); + } + + #[test] + fn an_unnamed_scope_claims_no_context() { + let renderer = ShortcutScopeRenderer; + assert_eq!(renderer.key_context(&node(serde_json::json!({}))), None); + assert_eq!( + renderer.key_context(&node(serde_json::json!({"name": ""}))), + None + ); + assert_eq!( + renderer.key_context(&node(serde_json::json!({"name": 7}))), + None + ); + } + + #[test] + fn scope_names_are_read_from_the_parsed_props() { + assert_eq!( + scope_name(&node(serde_json::json!({"name": "files"}))), + "files" + ); + assert_eq!(scope_name(&node(serde_json::json!({}))), ""); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/shortcuts/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/shortcuts/mod.rs new file mode 100644 index 0000000000..55ceffb362 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/shortcuts/mod.rs @@ -0,0 +1,124 @@ +//! The `GpuiShortcuts` TurboModule: how the JS registry installs its list. +//! +//! Every method is a thin wrapper over `desktop::keymap`, which owns the +//! shortcuts and the keymap rebuild. The list is the *only* thing JS sends: +//! parsing, precedence and dispatch are native, so a shortcut behaves the same +//! whether JS registered it through `useShortcut` or `Shortcut.addHandler`. + +use rn_gpui_module::{FromValue, ModuleError, ModuleThread, Path, Value}; + +use crate::builtins::main_thread::on_main; +use crate::builtins::rn_gpui_specs::{ + NativeGpuiShortcutsEvents, NativeGpuiShortcutsSpec, ShortcutInstallResult, ShortcutSpec, +}; +use crate::desktop::keymap; + +const MODULE: &str = "GpuiShortcuts"; + +struct ShortcutsModule; + +fn value_json(value: Value) -> serde_json::Value { + match value { + Value::Null | Value::Callback(_) => serde_json::Value::Null, + Value::Bool(flag) => serde_json::Value::Bool(flag), + Value::Number(number) => serde_json::Number::from_f64(number) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + Value::String(text) => serde_json::Value::String(text), + Value::Array(items) => { + serde_json::Value::Array(items.into_iter().map(value_json).collect()) + } + Value::Object(fields) => serde_json::Value::Object( + fields + .into_iter() + .map(|(key, item)| (key, value_json(item))) + .collect(), + ), + Value::Buffer(bytes) => serde_json::Value::Array( + bytes + .into_iter() + .map(|byte| serde_json::Value::Number(byte.into())) + .collect(), + ), + } +} + +fn json_value(value: &serde_json::Value) -> Value { + match value { + serde_json::Value::Null => Value::Null, + serde_json::Value::Bool(flag) => Value::Bool(*flag), + serde_json::Value::Number(number) => { + number.as_f64().map(Value::Number).unwrap_or(Value::Null) + } + serde_json::Value::String(text) => Value::String(text.clone()), + serde_json::Value::Array(items) => Value::Array(items.iter().map(json_value).collect()), + serde_json::Value::Object(fields) => Value::Object( + fields + .iter() + .map(|(key, item)| (key.clone(), json_value(item))) + .collect(), + ), + } +} + +fn typed(method: &'static str, value: &serde_json::Value) -> Result { + let mut path = Path::result(MODULE, method); + T::from_value(json_value(value), &mut path).map_err(|error| ModuleError::new(error.message)) +} + +impl NativeGpuiShortcutsSpec for ShortcutsModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiShortcutsEvents) -> Self { + Self + } + + async fn shortcut_install( + &self, + shortcuts: Vec, + ) -> Result { + // The generated spec struct is turned back into the JSON the parser + // reads, so there is one implementation of "what a shortcut is" and the + // module cannot drift from `keymap::Shortcut`. + let json = serde_json::Value::Array( + shortcuts + .into_iter() + .map(|spec| { + serde_json::json!({ + "id": spec.id, + "keys": spec.keys, + "scope": spec.scope, + "enabled": spec.enabled, + "allowInTextInput": spec.allow_in_text_input, + }) + }) + .collect(), + ); + // Bad input is reported, not swallowed: an app that spelled a key wrong + // has a shortcut that does not fire, and the error says which. + let parsed = keymap::parse_shortcuts(&json).map_err(ModuleError::new)?; + let installed = parsed.len(); + on_main(move |cx| { + keymap::install(cx, parsed); + }) + .await?; + Ok(ShortcutInstallResult { + installed: installed as f64, + }) + } + + async fn shortcut_get(&self) -> Result, ModuleError> { + let value = on_main(|cx| keymap::shortcuts_json(cx)).await?; + typed(MODULE, &value) + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +pub fn register() { + rn_gpui_module::register( + crate::builtins::rn_gpui_specs::native_gpui_shortcuts_module::(), + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/input.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/input.rs new file mode 100644 index 0000000000..0f0caa8fb2 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/input.rs @@ -0,0 +1,845 @@ +use crate::node_view::RNNodeView; +use crate::store::store_from; +use gpui::{ + Bounds, Context, EntityInputHandler, Pixels, Point, TextInputConfiguration, UTF16Selection, + Window, +}; +use std::ops::Range; + +pub(crate) fn handle_key( + this: &mut RNNodeView, + key: &str, + extend_selection: bool, + window: &mut Window, + cx: &mut Context, +) -> bool { + if !this.input_editable { + return false; + } + match key { + "backspace" | "delete" => { + let mut range = selected_range(this); + if range.is_empty() { + if key == "backspace" { + range.start = this.input_text[..range.start] + .char_indices() + .next_back() + .map(|(index, _)| index) + .unwrap_or(0); + } else if range.end < this.input_text.len() { + range.end += this.input_text[range.end..] + .chars() + .next() + .map(char::len_utf8) + .unwrap_or(0); + } + } + if !range.is_empty() { + this.input_text.replace_range(range.clone(), ""); + this.input_selection = range.start..range.start; + this.input_selection_anchor = None; + this.input_marked = None; + this.sync_input_paragraph(); + emit_change(this, window, cx); + } + true + } + "enter" => { + if this.input_multiline { + replace_plain_text( + &mut this.input_text, + &mut this.input_selection, + &mut this.input_marked, + None, + "\n", + ); + this.sync_input_paragraph(); + emit_change(this, window, cx); + } else if let Some(store) = store_from(cx) { + store.read(cx).host.dispatch_event( + this.tag, + "topSubmitEditing", + &serde_json::json!({ + "text": this.input_text, "eventCount": this.input_event_count, + }), + ); + } + true + } + "left" | "right" | "home" | "end" => { + let text = this.input_text.as_str(); + let selection = selected_range(this); + let (anchor, focus) = if extend_selection { + let anchor = *this.input_selection_anchor.get_or_insert(selection.start); + let active = if selection.is_empty() { + selection.end + } else if anchor == selection.start { + selection.end + } else { + selection.start + }; + let next = match key { + "left" => text[..active] + .char_indices() + .next_back() + .map(|(index, _)| index) + .unwrap_or(0), + "right" => { + active + + text[active..] + .chars() + .next() + .map(char::len_utf8) + .unwrap_or(0) + } + "home" => text[..active] + .rfind('\n') + .map(|index| index + 1) + .unwrap_or(0), + "end" => active + text[active..].find('\n').unwrap_or(text.len() - active), + _ => active, + }; + (anchor, next) + } else if !selection.is_empty() { + this.input_selection_anchor = None; + let cursor = if key == "left" || key == "home" { + selection.start + } else { + selection.end + }; + (cursor, cursor) + } else { + this.input_selection_anchor = None; + let cursor = selection.end; + let next = match key { + "left" => text[..cursor] + .char_indices() + .next_back() + .map(|(index, _)| index) + .unwrap_or(0), + "right" => { + cursor + + text[cursor..] + .chars() + .next() + .map(char::len_utf8) + .unwrap_or(0) + } + "home" => text[..cursor] + .rfind('\n') + .map(|index| index + 1) + .unwrap_or(0), + "end" => cursor + text[cursor..].find('\n').unwrap_or(text.len() - cursor), + _ => cursor, + }; + (next, next) + }; + this.input_selection = anchor.min(focus)..anchor.max(focus); + this.input_marked = None; + emit_selection(this, window, cx); + true + } + _ => false, + } +} + +pub(crate) fn select_at_point( + this: &mut RNNodeView, + point: Point, + extend: bool, + window: &mut Window, + cx: &mut Context, +) { + if this.component != "TextInput" || !this.input_editable { + return; + } + let index = this + .character_index_for_point(point, window, cx) + .map(|index| byte_at_utf16(&this.input_text, index)) + .unwrap_or(this.input_text.len()); + let anchor = if extend { + this.input_selection_anchor + .unwrap_or(this.input_selection.start) + } else { + index + }; + this.input_selection_anchor = Some(anchor); + this.input_selection = anchor.min(index)..anchor.max(index); + emit_selection(this, window, cx); +} + +pub(crate) fn select_all(this: &mut RNNodeView, window: &mut Window, cx: &mut Context) { + if this.component != "TextInput" || !this.input_editable { + return; + } + this.input_selection = 0..this.input_text.len(); + this.input_selection_anchor = None; + emit_selection(this, window, cx); +} + +pub(crate) fn copy_selection(this: &RNNodeView, cx: &mut gpui::App) -> bool { + if this.component != "TextInput" { + return false; + } + let range = selected_range(this); + if range.is_empty() { + return false; + } + let text = this.input_text[range].to_string(); + cx.write_to_clipboard(gpui::ClipboardItem::new_string(text)); + true +} + +pub(crate) fn cut_selection( + this: &mut RNNodeView, + window: &mut Window, + cx: &mut Context, +) -> bool { + if !this.input_editable || !copy_selection(this, cx) { + return false; + } + let range = selected_range(this); + this.input_text.replace_range(range.clone(), ""); + this.input_selection = range.start..range.start; + this.input_selection_anchor = None; + this.input_marked = None; + this.sync_input_paragraph(); + emit_change(this, window, cx); + true +} + +pub(crate) fn paste_clipboard( + this: &mut RNNodeView, + window: &mut Window, + cx: &mut Context, +) -> bool { + if !this.input_editable { + return false; + } + let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else { + return false; + }; + if text.is_empty() { + return false; + } + replace_plain_text( + &mut this.input_text, + &mut this.input_selection, + &mut this.input_marked, + None, + &text, + ); + this.sync_input_paragraph(); + emit_change(this, window, cx); + true +} + +pub(crate) fn byte_at_utf16(text: &str, offset: usize) -> usize { + let mut units = 0; + for (byte, ch) in text.char_indices() { + if units >= offset { + return byte; + } + let next = units + ch.len_utf16(); + if next > offset { + return byte; + } + units = next; + } + text.len() +} + +fn utf16_at_byte(text: &str, offset: usize) -> usize { + let offset = offset.min(text.len()); + text.char_indices() + .take_while(|(byte, _)| *byte < offset) + .map(|(_, ch)| ch.len_utf16()) + .sum() +} + +fn byte_range(text: &str, range: Range) -> Range { + let start = byte_at_utf16(text, range.start); + let end = byte_at_utf16(text, range.end.max(range.start)); + start..end.max(start) +} + +pub(crate) fn reconcile_selection( + text: &str, + current: Range, + explicit_utf16: Option>, + initialize: bool, +) -> Range { + if let Some(selection) = explicit_utf16 { + byte_range(text, selection) + } else if initialize { + text.len()..text.len() + } else { + current.start.min(text.len())..current.end.min(text.len()) + } +} + +pub(crate) fn should_apply_text_update(native_event_count: i32, props_event_count: i32) -> bool { + props_event_count >= native_event_count +} + +fn utf16_range(text: &str, range: Range) -> Range { + utf16_at_byte(text, range.start)..utf16_at_byte(text, range.end) +} + +fn selected_range(this: &RNNodeView) -> Range { + this.input_selection.start.min(this.input_text.len()) + ..this.input_selection.end.min(this.input_text.len()) +} + +fn replacement_range( + text: &str, + selection: &Range, + marked: &mut Option>, + range_utf16: Option>, +) -> Range { + range_utf16 + .map(|range| byte_range(text, range)) + .or_else(|| marked.take()) + .unwrap_or_else(|| selection.clone()) +} + +fn replace_plain_text( + text: &mut String, + selection: &mut Range, + marked: &mut Option>, + range_utf16: Option>, + inserted: &str, +) { + let range = replacement_range(text, selection, marked, range_utf16); + text.replace_range(range.clone(), inserted); + let cursor = range.start + inserted.len(); + *selection = cursor..cursor; + *marked = None; +} + +fn replace_marked_text( + text: &mut String, + selection: &mut Range, + marked: &mut Option>, + range_utf16: Option>, + inserted: &str, + selected_range_utf16: Option>, +) { + let range = replacement_range(text, selection, marked, range_utf16); + text.replace_range(range.clone(), inserted); + let composing = range.start..range.start + inserted.len(); + *marked = (!inserted.is_empty()).then_some(composing.clone()); + *selection = selected_range_utf16 + .map(|selected| { + let selected = byte_range(inserted, selected); + range.start + selected.start..range.start + selected.end + }) + .unwrap_or(composing.end..composing.end); +} + +fn clear_text_state( + text: &mut String, + selection: &mut Range, + marked: &mut Option>, +) -> bool { + if text.is_empty() { + return false; + } + text.clear(); + *selection = 0..0; + *marked = None; + true +} + +fn emit_change(this: &mut RNNodeView, window: &mut Window, cx: &mut Context) { + this.input_event_count += 1; + this.input_selection_anchor = None; + keep_caret_visible(this, window); + let text = this.input_text.clone(); + let selection = utf16_range(&text, selected_range(this)); + if let Some(store) = store_from(cx) { + let host = store.read(cx).host.clone(); + host.dispatch_event( + this.tag, + "topChange", + &serde_json::json!({ + "text": text, + "eventCount": this.input_event_count, + }), + ); + host.dispatch_event( + this.tag, + "topSelectionChange", + &serde_json::json!({"selection": {"start": selection.start, "end": selection.end}}), + ); + store.read(cx).frame_hook.arm(window); + } + this.reset_text_input_caret(cx); + cx.notify(); +} + +fn emit_selection(this: &mut RNNodeView, window: &mut Window, cx: &mut Context) { + keep_caret_visible(this, window); + let selection = utf16_range(&this.input_text, selected_range(this)); + if let Some(store) = store_from(cx) { + let host = store.read(cx).host.clone(); + host.dispatch_event( + this.tag, + "topSelectionChange", + &serde_json::json!({"selection": {"start": selection.start, "end": selection.end}}), + ); + store.read(cx).frame_hook.arm(window); + } + this.reset_text_input_caret(cx); + cx.notify(); +} + +pub(crate) fn keep_caret_visible(this: &mut RNNodeView, window: &mut Window) { + if this.input_multiline { + this.input_scroll_x = 0.0; + return; + } + let Some(paragraph) = this.paragraph.as_ref() else { + this.input_scroll_x = 0.0; + return; + }; + if paragraph.fragments.is_empty() { + this.input_scroll_x = 0.0; + return; + } + let end = this.input_selection.end.min(this.input_text.len()); + let line_start = this.input_text[..end] + .rfind('\n') + .map_or(0, |index| index + 1); + let line = &this.input_text[line_start..end]; + let caret_x = crate::text::input_caret_x(window.text_system(), paragraph, line, line.len()); + let viewport = + (this.frame.w - this.view.content_insets.left - this.view.content_insets.right).max(0.0); + this.input_scroll_x = input_scroll_offset(this.input_scroll_x, caret_x, viewport); +} + +fn input_scroll_offset(current: f32, caret_x: f32, viewport: f32) -> f32 { + let current = current.max(0.0); + let viewport = viewport.max(0.0); + if caret_x < current { + caret_x.max(0.0) + } else if caret_x > current + viewport { + (caret_x - viewport + 1.0).max(0.0) + } else { + current + } +} + +pub fn clear_text_input(this: &mut RNNodeView, window: &mut Window, cx: &mut Context) { + if this.component != "TextInput" + || !clear_text_state( + &mut this.input_text, + &mut this.input_selection, + &mut this.input_marked, + ) + { + return; + } + this.sync_input_paragraph(); + emit_change(this, window, cx); +} + +impl EntityInputHandler for RNNodeView { + fn text_for_range( + &mut self, + range_utf16: Range, + actual_range: &mut Option>, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let range = byte_range(&self.input_text, range_utf16); + *actual_range = Some(utf16_range(&self.input_text, range.clone())); + Some(self.input_text[range].to_owned()) + } + + fn selected_text_range( + &mut self, + _ignore_disabled_input: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + Some(UTF16Selection { + range: utf16_range(&self.input_text, selected_range(self)), + reversed: false, + }) + } + + fn marked_text_range( + &self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + self.input_marked + .as_ref() + .map(|range| utf16_range(&self.input_text, range.clone())) + } + + fn unmark_text(&mut self, _window: &mut Window, cx: &mut Context) { + self.input_marked = None; + cx.notify(); + } + + fn replace_text_in_range( + &mut self, + range_utf16: Option>, + text: &str, + window: &mut Window, + cx: &mut Context, + ) { + if !self.input_editable { + return; + } + if !self.input_multiline && (text.contains('\n') || text.contains('\r')) { + if let Some(store) = store_from(cx) { + let host = store.read(cx).host.clone(); + host.dispatch_event( + self.tag, + "topSubmitEditing", + &serde_json::json!({ + "text": self.input_text, + "eventCount": self.input_event_count, + }), + ); + } + return; + } + replace_plain_text( + &mut self.input_text, + &mut self.input_selection, + &mut self.input_marked, + range_utf16, + text, + ); + self.sync_input_paragraph(); + emit_change(self, window, cx); + } + + fn replace_and_mark_text_in_range( + &mut self, + range_utf16: Option>, + new_text: &str, + new_selected_range_utf16: Option>, + window: &mut Window, + cx: &mut Context, + ) { + if !self.input_editable { + return; + } + replace_marked_text( + &mut self.input_text, + &mut self.input_selection, + &mut self.input_marked, + range_utf16, + new_text, + new_selected_range_utf16, + ); + self.sync_input_paragraph(); + emit_change(self, window, cx); + } + + fn bounds_for_range( + &mut self, + range_utf16: Range, + element_bounds: Bounds, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { + let _ = range_utf16; + Some(element_bounds) + } + + fn character_index_for_point( + &mut self, + point: Point, + window: &mut Window, + cx: &mut Context, + ) -> Option { + let origin = self.absolute_origin(cx); + let (scale_x, scale_y) = self.cumulative_scale(cx); + let font_size = self + .paragraph + .as_ref() + .and_then(|paragraph| paragraph.fragments.first()) + .map(|fragment| fragment.font_size.max(14.0)) + .unwrap_or(14.0); + let line_height = self + .paragraph + .as_ref() + .and_then(|paragraph| paragraph.fragments.first()) + .map(|fragment| { + if fragment.line_height > 0.0 { + fragment.line_height + } else { + font_size * 1.2 + } + }) + .unwrap_or(font_size * 1.2); + let local_x = (f32::from(point.x - origin.x) / scale_x.max(0.0001) + - self.view.content_insets.left) + .max(0.0); + let local_x = if self.input_multiline { + local_x + } else { + local_x + self.input_scroll_x + }; + let local_y = (f32::from(point.y - origin.y) / scale_y.max(0.0001) + - self.view.content_insets.top) + .max(0.0); + let target_line = if self.input_multiline { + (local_y / line_height.max(1.0)).floor() as usize + } else { + 0 + }; + let mut byte_start = 0; + let mut line_text = self.input_text.as_str(); + if self.input_multiline { + for (index, line) in self.input_text.split_inclusive('\n').enumerate() { + if index == target_line { + line_text = line.trim_end_matches('\n'); + break; + } + byte_start += line.len(); + line_text = ""; + } + } + let byte_in_line = self + .paragraph + .as_ref() + .map(|paragraph| { + crate::text::input_index_for_x(window.text_system(), paragraph, line_text, local_x) + }) + .unwrap_or(line_text.len()); + Some(utf16_at_byte(&self.input_text, byte_start + byte_in_line)) + } + + fn set_selected_text_range( + &mut self, + range_utf16: Range, + _window: &mut Window, + cx: &mut Context, + ) { + self.input_selection = byte_range(&self.input_text, range_utf16); + self.input_selection_anchor = None; + emit_selection(self, _window, cx); + } + + fn text_length_utf16( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + Some(self.input_text.encode_utf16().count()) + } + + fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context) -> bool { + self.input_editable + } + + fn text_input_configuration( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> TextInputConfiguration { + TextInputConfiguration::default() + } +} + +#[cfg(test)] +mod tests { + use super::{ + byte_at_utf16, clear_text_state, input_scroll_offset, reconcile_selection, + replace_marked_text, replace_plain_text, should_apply_text_update, utf16_at_byte, + }; + + #[test] + fn utf16_offsets_round_trip_as_unicode_scalar_boundaries() { + let text = "a😀é"; + for offset in [0, 1, 3, 4] { + let byte = byte_at_utf16(text, offset); + assert_eq!(utf16_at_byte(text, byte), offset); + } + assert_eq!(byte_at_utf16(text, 2), 1); + } + + #[test] + fn plain_replacement_handles_a_selected_surrogate_pair() { + let mut text = "A😀Z".to_owned(); + let mut selection = 1..5; + let mut marked = None; + + replace_plain_text(&mut text, &mut selection, &mut marked, Some(1..3), "é"); + + assert_eq!(text, "AéZ"); + assert_eq!(selection, 3..3); + assert_eq!(marked, None); + } + + #[test] + fn marked_composition_replaces_its_previous_marked_text() { + let mut text = "Hi ".to_owned(); + let mut selection = 3..3; + let mut marked = None; + + replace_marked_text(&mut text, &mut selection, &mut marked, None, "k", None); + assert_eq!(text, "Hi k"); + assert_eq!(marked, Some(3..4)); + + replace_marked_text( + &mut text, + &mut selection, + &mut marked, + None, + "か", + Some(1..1), + ); + assert_eq!(text, "Hi か"); + assert_eq!(marked, Some(3..6)); + assert_eq!(selection, 6..6); + + replace_plain_text(&mut text, &mut selection, &mut marked, None, ""); + assert_eq!(text, "Hi "); + assert_eq!(marked, None); + } + + #[test] + fn prop_updates_preserve_internal_selection_unless_rn_sets_one() { + let middle = 1..1; + assert_eq!( + reconcile_selection("hello", middle.clone(), None, false), + middle + ); + assert_eq!(reconcile_selection("hello", 4..4, Some(1..3), false), 1..3); + assert_eq!(reconcile_selection("hello", 0..0, None, true), 5..5); + assert_eq!(reconcile_selection("hi", 4..4, None, false), 2..2); + } + + #[test] + fn stale_props_do_not_replace_text_after_a_newer_native_edit() { + assert!(!should_apply_text_update(3, 2)); + assert!(should_apply_text_update(3, 3)); + assert!(should_apply_text_update(3, 4)); + } + + #[test] + fn clear_command_resets_text_selection_and_composition() { + let mut text = "A😀".to_owned(); + let mut selection = 1..5; + let mut marked = Some(1..5); + assert!(clear_text_state(&mut text, &mut selection, &mut marked)); + assert_eq!(text, ""); + assert_eq!(selection, 0..0); + assert_eq!(marked, None); + assert!(!clear_text_state(&mut text, &mut selection, &mut marked)); + } + + #[test] + fn horizontal_scroll_keeps_the_caret_inside_the_viewport() { + assert_eq!(input_scroll_offset(0.0, 180.0, 100.0), 81.0); + assert_eq!(input_scroll_offset(81.0, 40.0, 100.0), 40.0); + assert_eq!(input_scroll_offset(40.0, 90.0, 100.0), 40.0); + } +} + +pub(crate) fn desktop_navigation_event(stroke: &gpui::Keystroke) -> Option<&'static str> { + let modifiers = stroke.modifiers; + let no_other = !modifiers.alt && !modifiers.platform && !modifiers.shift; + let chord_enabled = if cfg!(target_os = "macos") { + modifiers.platform && !modifiers.alt && !modifiers.control && !modifiers.shift + } else if modifiers.alt && !modifiers.control && !modifiers.platform && !modifiers.shift { + true + } else { + modifiers.control && no_other + }; + if !chord_enabled { + return None; + } + let (back, forward) = if cfg!(target_os = "macos") || modifiers.control { + ("[", "]") + } else { + ("left", "right") + }; + if stroke.key == back { + Some("hardwareBackPress") + } else if stroke.key == forward { + Some("hardwareForwardPress") + } else { + None + } +} + +/// `core-private:` this reads and writes `RNNodeView`'s input fields directly. +pub(crate) fn apply_update(this: &mut RNNodeView, op: &rn_gpui_sys::ffi::MountOp) { + let next = TextInputProps::from_json(&op.extension_props_json); + if should_apply_text_update(this.input_event_count, next.event_count) + && next.text != this.input_text + { + this.input_text.clone_from(&next.text); + this.input_marked = None; + } + this.input_placeholder = next.placeholder; + this.input_selection_color = next.selection_color; + this.input_multiline = next.multiline; + this.input_editable = next.editable; + this.input_selection = reconcile_selection( + &this.input_text, + this.input_selection.clone(), + next.selection, + false, + ); +} + +/// `core-private:` the node stores these as its own fields, so the module both +pub(crate) struct TextInputProps { + pub text: String, + pub placeholder: String, + pub selection_color: u32, + pub multiline: bool, + pub editable: bool, + pub secure: bool, + pub event_count: i32, + pub selection: Option>, +} + +impl TextInputProps { + pub(crate) fn from_json(json: &str) -> Self { + let value: serde_json::Value = + serde_json::from_str(json).unwrap_or(serde_json::Value::Null); + let string = |key: &str| { + value + .get(key) + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + .to_string() + }; + let flag = |key: &str, default: bool| { + value + .get(key) + .and_then(serde_json::Value::as_bool) + .unwrap_or(default) + }; + let number = |key: &str| { + value + .get(key) + .and_then(serde_json::Value::as_i64) + .unwrap_or_default() + }; + let selection = flag("selectionSet", false).then(|| { + number("selectionStart").max(0) as usize..number("selectionEnd").max(0) as usize + }); + Self { + text: string("text"), + placeholder: string("placeholder"), + selection_color: number("selectionColor") as u32, + multiline: flag("multiline", false), + editable: flag("editable", true), + secure: flag("secure", false), + event_count: number("eventCount") as i32, + selection, + } + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/mod.rs new file mode 100644 index 0000000000..a3ee7b090d --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/mod.rs @@ -0,0 +1,5 @@ +// core-private: the caret, the selection and the focus handle are `RNNodeView` +// core-private: the caret geometry comes from `crate::text`, which is core's own. + +pub(crate) mod input; +pub(crate) mod render; diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/render.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/render.rs new file mode 100644 index 0000000000..a4b36aed8b --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/textinput/render.rs @@ -0,0 +1,245 @@ +// core-private: these functions read `RNNodeView` state (`input_selection`, + +use gpui::{div, prelude::*, px, Div, Entity, ParentElement, Stateful, Window}; +use rn_gpui_sys::ffi::{ParagraphFlat, RectF}; + +use crate::node_view::RNNodeView; + +pub(crate) fn paragraph_content( + node: &RNNodeView, + mut element: Stateful
, + paragraph: &ParagraphFlat, + scale: (f32, f32), + frame: RectF, + window: &mut Window, +) -> Stateful
{ + let (scale_x, scale_y) = scale; + let left = node.view.content_insets.left * scale_x; + let top = node.view.content_insets.top * scale_y; + let right = node.view.content_insets.right * scale_x; + let bottom = node.view.content_insets.bottom * scale_y; + let viewport_width = (frame.w * scale_x - left - right).max(0.0); + let viewport_height = (frame.h * scale_y - top - bottom).max(0.0); + if !node.input_multiline { + let mut paragraph = paragraph.clone(); + paragraph.max_lines = 0; + paragraph.ellipsize = 0; + let line = node.input_text.split('\n').next().unwrap_or_default(); + let natural_width = + crate::text::input_caret_x(window.text_system(), ¶graph, line, line.len()); + let text_width = viewport_width.max(natural_width + 2.0); + element = element.child( + div() + .absolute() + .left(px(left)) + .top(px(top)) + .w(px(viewport_width)) + .h(px(viewport_height)) + .overflow_hidden() + .child( + div() + .absolute() + .left(px(-node.input_scroll_x * scale_x)) + .top_0() + .w(px(text_width)) + .h(px(viewport_height)) + .child(crate::text::paragraph_element( + ¶graph, + text_width, + viewport_height, + )), + ), + ); + } else { + element = element.child( + div() + .absolute() + .left(px(left)) + .top(px(top)) + .w(px(viewport_width)) + .h(px(viewport_height)) + .child(crate::text::paragraph_element( + paragraph, + viewport_width, + viewport_height, + )), + ); + } + element +} + +/// Fabric measured the text inside the padding and border, so it is laid out +/// and painted in that content box, not across the whole frame. +pub(crate) fn plain_paragraph( + node: &RNNodeView, + element: Stateful
, + paragraph: &ParagraphFlat, + scale: (f32, f32), + frame: RectF, +) -> Stateful
{ + let (scale_x, scale_y) = scale; + let insets = node.view.content_insets; + let left = insets.left * scale_x; + let top = insets.top * scale_y; + let width = (frame.w * scale_x - left - insets.right * scale_x).max(0.0); + let height = (frame.h * scale_y - top - insets.bottom * scale_y).max(0.0); + element.child( + div() + .absolute() + .left(px(left)) + .top(px(top)) + .w(px(width)) + .h(px(height)) + .child(crate::text::paragraph_element(paragraph, width, height)), + ) +} + +pub(crate) fn caret_canvas( + node: &RNNodeView, + element: Stateful
, + input: Entity, + scale: (f32, f32), +) -> Stateful
{ + let (scale_x, scale_y) = scale; + let focus = node.focus_handle.clone(); + let caret_text = node.input_text.clone(); + let caret_offset = node.input_selection.end.min(node.input_text.len()); + let mut caret_paragraph = node.paragraph.clone().unwrap_or_default(); + for fragment in &mut caret_paragraph.fragments { + fragment.font_size *= scale_y; + fragment.letter_spacing *= scale_x; + } + let input_selection = node.input_selection.clone(); + let content_insets = node.view.content_insets; + let input_text_color = node.input_text_color; + let input_selection_color = node.input_selection_color; + let input_multiline = node.input_multiline; + let input_scroll_x = node.input_scroll_x * scale_x; + let caret_visible = node.caret_visible; + let caret_height = node + .paragraph + .as_ref() + .and_then(|paragraph| paragraph.fragments.first()) + .map(|fragment| { + if fragment.line_height > 0.0 { + fragment.line_height * scale_y + } else { + fragment.font_size.max(14.0) * scale_y * 1.2 + } + }) + .unwrap_or(17.0 * scale_y); + element.child( + div().absolute().top_0().left_0().size_full().child( + gpui::canvas( + |_bounds, _window, _cx| (), + move |bounds, _state, window, cx| { + if focus.as_ref().is_some_and(|f| f.is_focused(window)) { + if !input_selection.is_empty() { + let start = input_selection.start.min(caret_text.len()); + let end = input_selection.end.min(caret_text.len()); + let content_width = (f32::from(bounds.size.width) + - (content_insets.left + content_insets.right) * scale_x) + .max(0.0); + let mut line_start = 0; + for (line_number, raw_line) in + caret_text.split_inclusive('\n').enumerate() + { + let line = raw_line.trim_end_matches('\n'); + let line_end = line_start + line.len(); + let selected_start = start.max(line_start).min(line_end); + let selected_end = end.min(line_end).max(selected_start); + let includes_newline = + end > line_end && start <= line_end && raw_line.ends_with('\n'); + if selected_end > selected_start || includes_newline { + let prefix = &line[..selected_start - line_start]; + let through_selection = &line[..selected_end - line_start]; + let mut x0 = crate::text::input_caret_x( + window.text_system(), + &caret_paragraph, + prefix, + prefix.len(), + ); + let mut x1 = crate::text::input_caret_x( + window.text_system(), + &caret_paragraph, + through_selection, + through_selection.len(), + ); + if !input_multiline { + x0 -= input_scroll_x; + x1 -= input_scroll_x; + x0 = x0.clamp(0.0, content_width); + x1 = x1.clamp(0.0, content_width); + } + let width = if includes_newline { + (content_width - x0).max(1.0) + } else { + (x1 - x0).max(1.0) + }; + let selection = gpui::Bounds { + origin: gpui::point( + bounds.origin.x + + px(content_insets.left * scale_x + x0), + bounds.origin.y + + px(content_insets.top * scale_y + + line_number as f32 * caret_height), + ), + size: gpui::size(px(width), px(caret_height)), + }; + window + .paint_quad(gpui::fill(selection, gpui::rgba(0x3875d788))); + } + line_start += raw_line.len(); + } + } + if caret_visible { + let offset = caret_offset.min(caret_text.len()); + let safe_offset = (0..=offset) + .rev() + .find(|index| caret_text.is_char_boundary(*index)) + .unwrap_or(0); + let prefix = &caret_text[..safe_offset]; + let line = prefix.rsplit('\n').next().unwrap_or(prefix); + let line_number = prefix.bytes().filter(|byte| *byte == b'\n').count(); + let x = crate::text::input_caret_x( + window.text_system(), + &caret_paragraph, + line, + line.len(), + ); + let mut x = x - input_scroll_x; + let content_width = (f32::from(bounds.size.width) + - (content_insets.left + content_insets.right) * scale_x) + .max(0.0); + if !input_multiline { + x = x.clamp(0.0, (content_width - scale_x.max(1.0)).max(0.0)); + } + let x = bounds.origin.x + px(content_insets.left * scale_x + x); + let y = bounds.origin.y + + px(content_insets.top * scale_y + + line_number as f32 * caret_height); + let caret = gpui::Bounds { + origin: gpui::point(x, y), + size: gpui::size(px(scale_x.max(1.0)), px(caret_height)), + }; + let caret_color = if input_selection_color == 0 { + input_text_color + } else { + input_selection_color + }; + window.paint_quad(gpui::fill(caret, crate::style::color(caret_color))); + } + } + if let Some(focus) = focus.as_ref() { + window.handle_input( + focus, + gpui::ElementInputHandler::new(bounds, input.clone()), + cx, + ); + } + }, + ) + .size_full(), + ), + ) +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/tray/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/tray/mod.rs new file mode 100644 index 0000000000..c89b7cfd91 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/tray/mod.rs @@ -0,0 +1,193 @@ +use rn_gpui_module::{FromValue, ModuleError, ModuleThread, Path, Value}; + +use crate::builtins::main_thread::on_main; +use crate::builtins::rn_gpui_specs::{ + NativeGpuiTrayEvents, NativeGpuiTraySpec, TrayHandle, TrayOptions, TrayState, +}; +use crate::desktop::tray; + +const MODULE: &str = "GpuiTray"; + +struct TrayModule; + +fn value_json(value: Value) -> serde_json::Value { + match value { + Value::Null | Value::Callback(_) => serde_json::Value::Null, + Value::Bool(flag) => serde_json::Value::Bool(flag), + Value::Number(number) => serde_json::Number::from_f64(number) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + Value::Buffer(bytes) => serde_json::Value::Array( + bytes + .into_iter() + .map(|byte| serde_json::Value::Number(byte.into())) + .collect(), + ), + Value::String(text) => serde_json::Value::String(text), + Value::Array(items) => { + serde_json::Value::Array(items.into_iter().map(value_json).collect()) + } + Value::Object(fields) => serde_json::Value::Object( + fields + .into_iter() + .map(|(key, item)| (key, value_json(item))) + .collect(), + ), + } +} + +fn json_value(value: &serde_json::Value) -> Value { + match value { + serde_json::Value::Null => Value::Null, + serde_json::Value::Bool(flag) => Value::Bool(*flag), + serde_json::Value::Number(number) => { + number.as_f64().map(Value::Number).unwrap_or(Value::Null) + } + serde_json::Value::String(text) => Value::String(text.clone()), + serde_json::Value::Array(items) => Value::Array(items.iter().map(json_value).collect()), + serde_json::Value::Object(fields) => Value::Object( + fields + .iter() + .map(|(key, item)| (key.clone(), json_value(item))) + .collect(), + ), + } +} + +fn json_items(items: Option>) -> Option { + items.map(|items| serde_json::Value::Array(items.into_iter().map(value_json).collect())) +} + +fn desktop_error(error: crate::desktop::errors::DesktopError) -> ModuleError { + ModuleError::new(error.message) +} + +fn typed(method: &'static str, value: &serde_json::Value) -> Result { + let mut path = Path::result(MODULE, method); + T::from_value(json_value(value), &mut path).map_err(|error| ModuleError::new(error.message)) +} + +fn void( + result: Result, +) -> Result<(), ModuleError> { + result + .map(|_| ()) + .map_err(|error| ModuleError::new(error.message)) +} + +impl NativeGpuiTraySpec for TrayModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiTrayEvents) -> Self { + Self + } + + async fn tray_install(&self, options: TrayOptions) -> Result { + let icon = match options.icon.as_deref() { + Some(uri) => Some( + crate::desktop::icon::load(uri) + .await + .map_err(ModuleError::new)?, + ), + None => None, + }; + let menu = json_items(options.menu); + if let Some(menu) = &menu { + crate::desktop::icon::preload_menu(menu).await; + } + let handle = on_main( + move |cx| -> Result { + let mut config = tray::TrayConfig::default(); + if let Some(id) = options.id { + config.id = id; + } + config.tooltip = options.tooltip; + config.title = options.title; + if let Some(icon) = icon { + config.icon = Some((icon.rgba.clone(), icon.width, icon.height)); + config.icon_is_template = options.icon_template.unwrap_or(false); + } + config.component = options.component; + if let Some(size) = options.size { + config = config.size(size.width as f32, size.height as f32); + } + if let Some(menu) = menu { + config.menu = Some(crate::desktop::menu_model::parse_menu(&menu)?); + } + if let Some(on_left_click) = options.menu_on_left_click { + config.menu_on_left_click = on_left_click; + } + if let Some(quit) = options.quit_on_last_window_closed { + config.quit_on_last_window_closed = quit; + } + if let Some(hide) = options.hide_window_on_close { + config.hide_window_on_close = hide; + } + tray::install(cx, config) + }, + ) + .await?; + typed("trayInstall", &handle.map_err(desktop_error)?) + } + + async fn tray_destroy(&self) -> Result<(), ModuleError> { + on_main(tray::destroy).await.and_then(void) + } + + async fn tray_get_state(&self) -> Result { + let value = on_main( + |cx| -> Result { + Ok(tray::state(cx)) + }, + ) + .await?; + typed("trayGetState", &value.map_err(desktop_error)?) + } + + async fn tray_set_tooltip(&self, tooltip: Option) -> Result<(), ModuleError> { + on_main(move |cx| tray::set_tooltip(cx, tooltip.as_deref())) + .await + .and_then(void) + } + + async fn tray_set_visible(&self, visible: bool) -> Result<(), ModuleError> { + on_main(move |cx| tray::set_visible(cx, visible)) + .await + .and_then(void) + } + + async fn tray_set_icon(&self, icon: String, template: bool) -> Result<(), ModuleError> { + let icon = crate::desktop::icon::load(&icon) + .await + .map_err(ModuleError::new)?; + on_main(move |cx| tray::set_icon(cx, &icon, template)) + .await + .and_then(void) + } + + async fn tray_set_title(&self, title: Option) -> Result<(), ModuleError> { + on_main(move |cx| tray::set_title(cx, title.as_deref())) + .await + .and_then(void) + } + + async fn tray_set_menu(&self, items: Option>) -> Result<(), ModuleError> { + let items = json_items(items); + if let Some(items) = &items { + crate::desktop::icon::preload_menu(items).await; + } + on_main(move |cx| tray::set_menu(cx, items.as_ref())) + .await + .and_then(void) + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +pub fn register() { + rn_gpui_module::register(crate::builtins::rn_gpui_specs::native_gpui_tray_module::< + TrayModule, + >()); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/window_control/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/window_control/mod.rs new file mode 100644 index 0000000000..772698149f --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/window_control/mod.rs @@ -0,0 +1,202 @@ +//! A caption button: minimize, maximize/restore, or close. +//! +//! The action runs natively, so it works while JS is busy, and on Windows the +//! area takes part in the OS hit-test — which is what gives the snap-layout +//! flyout over a custom maximize button. Only the appearance comes from JS: the +//! component's `style` and `children` may be functions of the state this reports. + +use std::sync::Arc; + +use gpui::{AnyElement, App, Window}; +use serde_json::Value; + +use crate::extensions::input::{ + InputContext, InputSpec, MouseEvent, Propagation, WindowControl as ControlArea, +}; +use crate::extensions::{register_component_renderer, ComponentNode, ComponentRenderer}; + +struct WindowControlRenderer; + +/// What `action` names, or `None` when the button is disabled. +/// +/// Disabled means no area as well as no action: on Windows the OS would otherwise +/// still draw the snap flyout over a button that does nothing. +fn claimed_area(props: &Value) -> Option { + if props + .get("disabled") + .and_then(Value::as_bool) + .unwrap_or(false) + { + return None; + } + match props.get("action").and_then(Value::as_str) { + Some("minimize") => Some(ControlArea::Minimize), + Some("maximize") => Some(ControlArea::Maximize), + Some("close") => Some(ControlArea::Close), + // The prop has a native default of `close`, and core reads the parsed + // props, so anything else is a button with nothing to do. + _ => None, + } +} + +impl ComponentRenderer for WindowControlRenderer { + fn render( + &self, + _node: &ComponentNode, + _window: &mut Window, + _cx: &mut App, + ) -> Option { + // Core paints the element, the style and the children; this renderer only + // claims the area and tracks state. + None + } + + fn window_control(&self, node: &ComponentNode) -> Option { + claimed_area(&node.props) + } + + fn input(&self, node: &ComponentNode) -> InputSpec { + if claimed_area(&node.props).is_none() { + return InputSpec::default(); + } + InputSpec { + mouse_buttons: crate::extensions::input::ButtonSet::one(crate::events::Button::Left), + hover: true, + cursor: Some(crate::extensions::input::CursorStyle::Pointer), + ..InputSpec::default() + } + } + + fn on_hover(&self, node: &ComponentNode, hovered: bool, cx: &mut InputContext<'_>) { + send_state(node, hovered, false, cx); + } + + fn on_mouse_down( + &self, + node: &ComponentNode, + _event: &MouseEvent, + cx: &mut InputContext<'_>, + ) -> Propagation { + send_state(node, true, true, cx); + // Capture so a release outside the button cancels rather than acting. + cx.capture_pointer(); + Propagation::Stop + } + + fn on_mouse_up( + &self, + node: &ComponentNode, + event: &MouseEvent, + cx: &mut InputContext<'_>, + ) -> Propagation { + cx.release_pointer(); + send_state(node, false, false, cx); + // Only a release inside the button acts, as a native caption button does. + let inside = event.local.x >= gpui::px(0.0) + && event.local.y >= gpui::px(0.0) + && event.local.x <= gpui::px(node.frame.w) + && event.local.y <= gpui::px(node.frame.h); + if !inside { + return Propagation::Stop; + } + // macOS and Linux act here; on Windows the OS hit-test performs the action + // and this would double it, so it is skipped. See `perform_action`. + perform_action(node, cx); + Propagation::Stop + } +} + +/// Sends `onControlStateChange`. +/// +/// Called on a hover edge, a press and a release — never for every pointer move — +/// because the edge is decided by the node's own `hovered` flag. +fn send_state(_node: &ComponentNode, hovered: bool, pressed: bool, cx: &mut InputContext<'_>) { + // No deduplication here: `on_hover` is only called on a real edge now that the + // previous state lives on the node, and a press or release is always a change. + // A second guess at it read a prop that never exists, so it never fired. + let maximized = cx.window_is_maximized(); + cx.dispatch_event( + "topControlStateChange", + &serde_json::json!({ + "hovered": hovered, + "pressed": pressed, + "maximized": maximized, + }), + ); +} + +/// Performs the button's action, on the platforms where core owns it. +fn perform_action(node: &ComponentNode, cx: &mut InputContext<'_>) { + // On Windows the OS performs the action through the non-client hit-test, which + // is what makes snap layouts work. Doing it here too would run it twice. + if std::env::consts::OS == "windows" { + return; + } + match claimed_area(&node.props) { + Some(ControlArea::Minimize) => cx.minimize_window(), + Some(ControlArea::Maximize) => cx.toggle_maximize_window(), + Some(ControlArea::Close) => cx.close_window(), + _ => {} + } +} + +pub fn register() { + register_component_renderer("DesktopWindowControl", Arc::new(WindowControlRenderer)); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn node(props: Value) -> ComponentNode { + ComponentNode { + component: "DesktopWindowControl".to_string(), + tag: 1, + frame: rn_gpui_sys::ffi::RectF { + x: 0.0, + y: 0.0, + w: 34.0, + h: 26.0, + }, + scale: (1.0, 1.0), + props: Arc::new(props), + parent: None, + children: Vec::new(), + } + } + + #[test] + fn each_action_claims_its_area() { + assert_eq!( + claimed_area(&serde_json::json!({ "action": "minimize" })), + Some(ControlArea::Minimize) + ); + assert_eq!( + claimed_area(&serde_json::json!({ "action": "maximize" })), + Some(ControlArea::Maximize) + ); + assert_eq!( + claimed_area(&serde_json::json!({ "action": "close" })), + Some(ControlArea::Close) + ); + } + + #[test] + fn a_disabled_button_claims_nothing() { + // No area means no hover, no press and no snap flyout on Windows: a + // caption button that does nothing must not look like it does. + assert_eq!( + claimed_area(&serde_json::json!({ "action": "close", "disabled": true })), + None + ); + } + + #[test] + fn an_unknown_action_claims_nothing() { + assert_eq!( + claimed_area(&serde_json::json!({ "action": "explode" })), + None + ); + assert_eq!(claimed_area(&serde_json::json!({})), None); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/windows/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/windows/mod.rs new file mode 100644 index 0000000000..f574683845 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/builtins/windows/mod.rs @@ -0,0 +1,306 @@ +use rn_gpui_module::{FromValue, ModuleError, ModuleThread, Path, Value}; + +use crate::bootstrap::WindowTitleBar; +use crate::builtins::main_thread::on_main; +use crate::builtins::rn_gpui_specs::{ + NativeGpuiWindowsEvents, NativeGpuiWindowsSpec, WindowHandle, WindowList, WindowOpenOptions, + WindowState, +}; +use crate::desktop::errors::{DesktopError, DesktopResult}; +use crate::desktop::windows; + +const MODULE: &str = "GpuiWindows"; + +struct WindowsModule; + +fn window_key(window_id: i32) -> u32 { + window_id as u32 +} + +fn value_json(value: Value) -> serde_json::Value { + match value { + Value::Null | Value::Callback(_) => serde_json::Value::Null, + Value::Bool(flag) => serde_json::Value::Bool(flag), + Value::Number(number) => serde_json::Number::from_f64(number) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + Value::String(text) => serde_json::Value::String(text), + Value::Array(items) => { + serde_json::Value::Array(items.into_iter().map(value_json).collect()) + } + Value::Object(fields) => serde_json::Value::Object( + fields + .into_iter() + .map(|(key, item)| (key, value_json(item))) + .collect(), + ), + Value::Buffer(bytes) => serde_json::Value::Array( + bytes + .into_iter() + .map(|byte| serde_json::Value::Number(byte.into())) + .collect(), + ), + } +} + +fn json_value(value: &serde_json::Value) -> Value { + match value { + serde_json::Value::Null => Value::Null, + serde_json::Value::Bool(flag) => Value::Bool(*flag), + serde_json::Value::Number(number) => { + number.as_f64().map(Value::Number).unwrap_or(Value::Null) + } + serde_json::Value::String(text) => Value::String(text.clone()), + serde_json::Value::Array(items) => Value::Array(items.iter().map(json_value).collect()), + serde_json::Value::Object(fields) => Value::Object( + fields + .iter() + .map(|(key, item)| (key.clone(), json_value(item))) + .collect(), + ), + } +} + +fn module_error(error: DesktopError) -> ModuleError { + ModuleError::new(error.message) +} + +fn typed(method: &'static str, result: DesktopResult) -> Result { + let mut path = Path::result(MODULE, method); + let value = json_value(&result.map_err(module_error)?); + T::from_value(value, &mut path).map_err(|error| ModuleError::new(error.message)) +} + +fn void(result: DesktopResult) -> Result<(), ModuleError> { + result.map(|_| ()).map_err(module_error) +} + +impl NativeGpuiWindowsSpec for WindowsModule { + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiWindowsEvents) -> Self { + Self + } + + async fn window_open(&self, options: WindowOpenOptions) -> Result { + let result = on_main(move |cx| -> DesktopResult { + let always_on_top = options.always_on_top.unwrap_or(false); + let open = windows::OpenWindowOptions { + component: options.component, + props: options + .props + .map(value_json) + .filter(|value| value.is_object()), + title: options.title, + width: options.width.map(|value| value as f32), + height: options.height.map(|value| value as f32), + x: options.x.map(|value| value as f32), + y: options.y.map(|value| value as f32), + resizable: options.resizable.unwrap_or(true), + // The same enum the main window's `desktop.json` maps to, so + // `transparent` and `hidden` mean one thing in both places. + title_bar: options + .title_bar + .as_deref() + .map(WindowTitleBar::from_name) + .unwrap_or_default(), + traffic_light_position: options + .traffic_light_position + .map(|position| (position.x as f32, position.y as f32)), + role: windows::WindowRoleOverride::Secondary, + popup: false, + }; + let key = windows::open_window(cx, open)?; + if always_on_top { + windows::set_always_on_top(cx, key, true)?; + } + Ok(serde_json::json!({ "windowId": key })) + }) + .await?; + typed("windowOpen", result) + } + + async fn window_list(&self) -> Result { + let result = on_main(|cx| windows::list_windows(cx)).await?; + typed("windowList", result) + } + + async fn window_get_state(&self, window_id: i32) -> Result { + let result = on_main(move |cx| windows::window_state(cx, window_key(window_id))).await?; + typed("windowGetState", result) + } + + async fn window_close(&self, window_id: i32, force: bool) -> Result<(), ModuleError> { + let result = on_main(move |cx| windows::close(cx, window_key(window_id), force)).await?; + void(result) + } + + async fn window_focus(&self, window_id: i32) -> Result<(), ModuleError> { + on_main(move |cx| windows::focus(cx, window_key(window_id))) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_set_title(&self, window_id: i32, title: String) -> Result<(), ModuleError> { + on_main(move |cx| windows::set_title(cx, window_key(window_id), title)) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_set_size( + &self, + window_id: i32, + width: f64, + height: f64, + ) -> Result<(), ModuleError> { + on_main(move |cx| windows::set_size(cx, window_key(window_id), width as f32, height as f32)) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_set_position(&self, window_id: i32, x: f64, y: f64) -> Result<(), ModuleError> { + on_main(move |cx| windows::set_position(cx, window_key(window_id), x as f32, y as f32)) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_set_min_size( + &self, + window_id: i32, + width: f64, + height: f64, + ) -> Result<(), ModuleError> { + on_main(move |cx| { + windows::set_min_size(cx, window_key(window_id), width as f32, height as f32) + }) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_set_max_size( + &self, + window_id: i32, + width: f64, + height: f64, + ) -> Result<(), ModuleError> { + on_main(move |cx| { + windows::set_max_size(cx, window_key(window_id), width as f32, height as f32) + }) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_set_resizable( + &self, + window_id: i32, + resizable: bool, + ) -> Result<(), ModuleError> { + on_main(move |cx| windows::set_resizable(cx, window_key(window_id), resizable)) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_set_fullscreen( + &self, + window_id: i32, + fullscreen: bool, + ) -> Result<(), ModuleError> { + on_main(move |cx| windows::set_fullscreen(cx, window_key(window_id), fullscreen)) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_set_always_on_top( + &self, + window_id: i32, + on_top: bool, + ) -> Result<(), ModuleError> { + on_main(move |cx| windows::set_always_on_top(cx, window_key(window_id), on_top)) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_minimize(&self, window_id: i32) -> Result<(), ModuleError> { + on_main(move |cx| windows::minimize(cx, window_key(window_id))) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_maximize(&self, window_id: i32) -> Result<(), ModuleError> { + on_main(move |cx| windows::maximize(cx, window_key(window_id))) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_unmaximize(&self, window_id: i32) -> Result<(), ModuleError> { + on_main(move |cx| windows::unmaximize(cx, window_key(window_id))) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_toggle_maximize(&self, window_id: i32) -> Result<(), ModuleError> { + on_main(move |cx| windows::toggle_maximize(cx, window_key(window_id))) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_show(&self, window_id: i32) -> Result<(), ModuleError> { + on_main(move |cx| windows::show(cx, window_key(window_id))) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_hide(&self, window_id: i32) -> Result<(), ModuleError> { + on_main(move |cx| windows::hide(cx, window_key(window_id))) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_start_dragging(&self, window_id: i32) -> Result<(), ModuleError> { + on_main(move |cx| windows::start_dragging(cx, window_key(window_id))) + .await? + .map(|_| ()) + .map_err(module_error) + } + + async fn window_intercept_close(&self, window_id: i32, on: bool) -> Result<(), ModuleError> { + on_main(move |cx| -> DesktopResult { + let key = window_key(window_id); + let entry = cx + .global_mut::() + .0 + .get_mut(key) + .ok_or_else(|| DesktopError::not_found(format!("window {key} is not open")))?; + entry.intercept_close = on; + Ok(serde_json::Value::Null) + }) + .await? + .map(|_| ()) + .map_err(module_error) + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +pub fn register() { + rn_gpui_module::register( + crate::builtins::rn_gpui_specs::native_gpui_windows_module::(), + ); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/counters.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/counters.rs new file mode 100644 index 0000000000..6df44e1cdc --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/counters.rs @@ -0,0 +1,78 @@ +//! Counters for the performance rules of host components and native surfaces. + +/// What is counted. +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +pub(crate) enum Counter { + /// Lookups in the host component registry, on either side of the bridge. + HostRegistryLookups, + /// Ancestors visited to keep the native-surface counts current. A tree with + /// no native surface visits none. + NativeSurfaceWalkNodes, + /// Bytes of a host component's props copied, serialised or parsed. + /// Only an `UnsafeMixed` prop read can add to it. + HostPropsBytesCopied, + /// Calls to a host component's `update`. + HostUpdateCalls, + /// Reports the store's delivery point applied. + HostReportsDelivered, +} + +#[cfg(feature = "test-driver")] +mod enabled { + use super::Counter; + use std::sync::atomic::{AtomicU64, Ordering}; + + const COUNT: usize = 5; + static VALUES: [AtomicU64; COUNT] = [ + AtomicU64::new(0), + AtomicU64::new(0), + AtomicU64::new(0), + AtomicU64::new(0), + AtomicU64::new(0), + ]; + + pub(crate) fn add(counter: Counter, amount: u64) { + VALUES[counter as usize].fetch_add(amount, Ordering::Relaxed); + } + + pub(crate) fn read(counter: Counter) -> u64 { + VALUES[counter as usize].load(Ordering::Relaxed) + } + + pub(crate) fn reset() { + for value in &VALUES { + value.store(0, Ordering::Relaxed); + } + } +} + +#[cfg(feature = "test-driver")] +pub(crate) use enabled::{read, reset}; + +/// Adds `amount` to `counter`. +#[cfg(feature = "test-driver")] +#[inline] +pub(crate) fn add(counter: Counter, amount: u64) { + enabled::add(counter, amount); +} + +/// Adds `amount` to `counter`. Nothing without the `test-driver` feature. +#[cfg(not(feature = "test-driver"))] +#[inline(always)] +pub(crate) fn add(_counter: Counter, _amount: u64) {} + +/// Counts one occurrence. +#[inline(always)] +pub(crate) fn bump(counter: Counter) { + add(counter, 1); +} + +/// Counts bytes of a host component's props that were copied, for the generated +/// accessor that reads an `UnsafeMixed` prop. Public because generated code in a +/// package calls it. +#[doc(hidden)] +#[inline] +pub fn note_props_bytes_copied(bytes: usize) { + add(Counter::HostPropsBytesCopied, bytes as u64); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/app_menu.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/app_menu.rs new file mode 100644 index 0000000000..6638060dd6 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/app_menu.rs @@ -0,0 +1,1500 @@ +//! GPUI derives an `NSMenuItem` key equivalent from the keymap binding for the +//! exact action value, and `Keymap` has no per-binding removal, so the desktop +//! bindings are filtered out and re-added whenever the menu changes. The Edit +//! roles act on the focused RN text input: `OsAction::Copy` needs an +//! `NSTextView` first responder, which GPUI-drawn inputs are not. + +use crate::desktop::errors::{DesktopError, DesktopResult}; +use crate::desktop::menu_model::{self, MenuItemDef, MenuModel, MenuNode, MenuRole, SubmenuDef}; +use crate::desktop::windows::DesktopWindowsGlobal; +use crate::node_view::RNNodeView; +use crate::store::store_from; +use gpui::{Action, App, Entity, KeyBinding, Menu, MenuItem, SharedString, SystemMenuType}; +use serde_json::{json, Value}; + +#[derive(Clone, Debug, PartialEq, gpui::Action)] +#[action(namespace = rn_gpui_desktop, no_json, no_register)] +pub struct MenuAccelerator { + pub id: String, +} + +#[derive(Clone, Debug, PartialEq, gpui::Action)] +#[action(namespace = rn_gpui_desktop, no_json, no_register)] +pub struct MenuEditAction { + pub kind: String, +} + +#[derive(Clone, Debug, PartialEq, gpui::Action)] +#[action(namespace = rn_gpui_desktop, no_json, no_register)] +pub struct MenuHostAction { + pub kind: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum AcceleratorAction { + Press(String), + Edit(String), + Host(String), + /// A Developer menu command, performed by the host like the dev menu's own + /// buttons. + Dev(String), +} + +pub struct AppMenuState { + pub model: MenuModel, + pub user_set: bool, + pub app_name: String, + pub bindings: Vec<(String, String)>, + pub accelerators: Vec<(gpui::Keystroke, AcceleratorAction)>, + pub _interceptor: Option, + pub rebuild_scheduled: bool, + /// Windows only: the attached `muda` menu bar and the HWND it is on. + /// + /// `muda::Menu::init_for_hwnd` installs a window subclass whose `dwRefData` + /// points into the `Menu`, so the `Menu` has to outlive the subclass. The + /// value is held here, in a global that lives as long as the app, and the + /// subclass is detached before it is replaced, otherwise the next message + /// this window receives (a resize, a paint) calls through a dangling + /// pointer and the process dies with `STATUS_STACK_BUFFER_OVERRUN`. + #[cfg(target_os = "windows")] + pub platform_menu: Option<(muda::Menu, isize)>, +} + +pub struct AppMenuGlobal(pub AppMenuState); +impl gpui::Global for AppMenuGlobal {} + +impl AppMenuGlobal { + pub fn new(app_name: String) -> Self { + Self(AppMenuState { + model: default_menu(&app_name), + user_set: false, + app_name, + bindings: Vec::new(), + accelerators: Vec::new(), + _interceptor: None, + rebuild_scheduled: false, + #[cfg(target_os = "windows")] + platform_menu: None, + }) + } +} + +/// Detaches the attached menu bar when the global is dropped, so a window that +/// outlives the menu state never keeps a subclass pointing into freed memory. +#[cfg(target_os = "windows")] +impl Drop for AppMenuGlobal { + fn drop(&mut self) { + if let Some((menu, hwnd)) = self.0.platform_menu.take() { + remove_platform_menu(menu, hwnd); + } + } +} + +pub fn install(cx: &mut App, app_name: &str, dev_menu: bool) { + cx.set_global(DevMenuEnabled(dev_menu)); + cx.set_global(AppMenuGlobal::new(app_name.to_string())); + let model = default_menu(app_name); + let _ = apply(cx, model, false); + cx.on_action(|action: &MenuAccelerator, cx: &mut App| { + trigger_item(cx, &action.id); + }); + cx.on_action(|action: &MenuEditAction, cx: &mut App| { + perform_edit(cx, &action.kind, None); + }); + cx.on_action(|action: &MenuHostAction, cx: &mut App| { + perform_host_action(cx, &action.kind, None); + }); + // The keymap binding is what makes macOS draw the shortcut in the NSMenu and + // what the menu item dispatches. Keystrokes are matched here as well, + // because a window with no focused element has no dispatch path for the + // keymap to resolve, and a global shortcut must work in that state too. + let interceptor = cx.intercept_keystrokes(|event, window, cx| { + // Yield to a shortcut. The keymap resolves a keystroke from the focused + // element's context, and a shortcut in a scope outranks the app-wide + // menu accelerator — but an interceptor runs *before* the keymap is + // consulted, so handling the key here would take it away from a shortcut + // that is entitled to it. Asking the keymap first is what keeps the + // precedence the rest of this design promises. + if crate::desktop::keymap::claims_keystroke(cx, window, &event.keystroke) { + return; + } + if handle_accelerator(cx, window, &event.keystroke) { + cx.stop_propagation(); + } + }); + cx.global_mut::().0._interceptor = Some(interceptor); +} + +pub fn trigger_item(cx: &mut App, id: &str) { + match accelerator_action(id) { + AcceleratorAction::Press(id) => emit_menu_press(cx, &id), + AcceleratorAction::Edit(kind) => perform_edit(cx, &kind, None), + AcceleratorAction::Host(kind) => perform_host_action(cx, &kind, None), + AcceleratorAction::Dev(id) => perform_dev_action(cx, id), + } +} + +pub fn handle_accelerator( + cx: &mut App, + window: &mut gpui::Window, + keystroke: &gpui::Keystroke, +) -> bool { + let action = { + let state = &cx.global::().0; + state + .accelerators + .iter() + .find(|(bound, _)| bound == keystroke) + .map(|(_, action)| action.clone()) + }; + if std::env::var_os("RN_GPUI_DEBUG_DESKTOP").is_some() { + // `RN_GPUI_DEBUG_DESKTOP=1` logs every keystroke the interceptor sees. + let installed = cx + .global::() + .0 + .accelerators + .iter() + .map(|(bound, _)| bound.to_string()) + .collect::>(); + log::info!( + "[desktop] keystroke {} matched={} installed=[{}]", + keystroke, + action.is_some(), + installed.join(", ") + ); + } + let Some(action) = action else { + return false; + }; + // The window comes from the keystroke, not `active_window()`: a hidden or + // off-screen window (the test driver's) is not the platform's active one. + let handle: gpui::AnyWindowHandle = window.window_handle().into(); + match action { + AcceleratorAction::Press(id) => emit_menu_press(cx, &id), + AcceleratorAction::Edit(kind) => perform_edit(cx, &kind, Some(handle)), + AcceleratorAction::Host(kind) => perform_host_action(cx, &kind, Some(handle)), + AcceleratorAction::Dev(id) => perform_dev_action(cx, id), + } + // The JS listener needs a frame to run in. + if let Some(store) = store_from(cx) { + store.read(cx).frame_hook.arm(window); + } + true +} + +pub fn set_application_menu(cx: &mut App, items: Option<&Value>) -> DesktopResult { + let model = match items { + Some(value) => menu_model::parse_menu(value)?, + None => default_menu(&app_name(cx)), + }; + let user_set = items.is_some(); + apply(cx, model, user_set)?; + Ok(json!({"items": current_model(cx).ids().len()})) +} + +pub fn update_item(cx: &mut App, id: &str, patch: &Value) -> DesktopResult { + let patch = patch + .as_object() + .ok_or_else(|| DesktopError::invalid_argument("a menu patch must be an object"))?; + { + let state = &mut cx.global_mut::().0; + let Some(item) = find_item_mut(&mut state.model.items, id) else { + return Err(DesktopError::not_found(format!( + "menu item {id} is not set" + ))); + }; + if let Some(label) = patch.get("label") { + item.label = label + .as_str() + .ok_or_else(|| DesktopError::invalid_argument("`label` must be a string"))? + .to_string(); + } + if let Some(enabled) = patch.get("enabled") { + item.enabled = enabled + .as_bool() + .ok_or_else(|| DesktopError::invalid_argument("`enabled` must be a boolean"))?; + } + if let Some(checked) = patch.get("checked") { + item.checked = checked + .as_bool() + .ok_or_else(|| DesktopError::invalid_argument("`checked` must be a boolean"))?; + } + } + schedule_rebuild(cx); + Ok(json!({"id": id})) +} + +/// Shows or hides the in-window menu bar of one window. +/// +/// Windows detaches the `muda` menu from the HWND rather than destroying it: the +/// menu stays alive in the global so the same items can be shown again, and +/// accelerators are GPUI key bindings that never went through the menu widget, so +/// they keep working either way. Linux skips attaching the bar and drops the +/// inset, which is what makes the React surface grow into the space. +pub fn set_menu_bar_visible(cx: &mut gpui::App, window_id: u32, visible: bool) -> DesktopResult { + crate::desktop::gpui_menu::set_menu_bar_visible(window_id, visible); + #[cfg(target_os = "windows")] + { + set_windows_menu_attached(cx, window_id, visible)?; + } + #[cfg(not(target_os = "windows"))] + { + let _ = cx; + } + // The surface size follows the bar, and the window has to relayout for the + // inset change to reach the React root. + crate::desktop::gpui_menu::resize_surfaces_for_menu_bar(cx, window_id); + Ok(json!({"windowId": window_id, "menuBarVisible": visible})) +} + +pub fn menu_json(cx: &App) -> Value { + let state = &cx.global::().0; + json!({ + "appName": state.app_name, + "userSet": state.user_set, + "items": nodes_json(&state.model.items), + // The main window's bar, which is the one an app asks about. A hidden + // bar is per window, so this reports the window the call is about. + "menuBarVisible": !crate::desktop::gpui_menu::menu_bar_hidden( + crate::desktop::windows::MAIN_WINDOW_KEY, + ), + }) +} + +fn nodes_json(items: &[MenuNode]) -> Vec { + items + .iter() + .map(|node| match node { + MenuNode::Separator => json!({"type": "separator"}), + MenuNode::StandardMenu(role) => { + json!({"type": "role", "role": role_name(*role)}) + } + MenuNode::Submenu(submenu) => json!({ + "type": "submenu", + "id": submenu.id, + "label": submenu.label, + "enabled": submenu.enabled, + "items": nodes_json(&submenu.items), + "icon": submenu.icon, + }), + MenuNode::Item(item) => json!({ + "type": "item", + "id": item.id, + "label": item.label, + "accelerator": item.accelerator, + "enabled": item.enabled, + "checked": item.checked, + "role": item.role.map(role_name), + "icon": item.icon, + }), + }) + .collect() +} + +fn role_name(role: MenuRole) -> &'static str { + match role { + MenuRole::About => "about", + MenuRole::Services => "services", + MenuRole::Hide => "hide", + MenuRole::HideOthers => "hideOthers", + MenuRole::ShowAll => "showAll", + MenuRole::Quit => "quit", + MenuRole::Undo => "undo", + MenuRole::Redo => "redo", + MenuRole::Cut => "cut", + MenuRole::Copy => "copy", + MenuRole::Paste => "paste", + MenuRole::SelectAll => "selectAll", + MenuRole::Minimize => "minimize", + MenuRole::Zoom => "zoom", + MenuRole::ToggleFullscreen => "toggleFullscreen", + MenuRole::Close => "close", + MenuRole::Help => "help", + MenuRole::AppMenu => "appMenu", + MenuRole::EditMenu => "editMenu", + MenuRole::WindowMenu => "windowMenu", + MenuRole::HelpMenu => "helpMenu", + } +} + +fn find_item_mut<'a>(items: &'a mut [MenuNode], id: &str) -> Option<&'a mut MenuItemDef> { + for node in items { + match node { + MenuNode::Item(item) if item.id == id => return Some(item), + MenuNode::Submenu(submenu) => { + if let Some(found) = find_item_mut(&mut submenu.items, id) { + return Some(found); + } + } + _ => {} + } + } + None +} + +pub fn current_model(cx: &App) -> MenuModel { + cx.global::().0.model.clone() +} + +pub fn app_name(cx: &App) -> String { + cx.global::().0.app_name.clone() +} + +pub fn standard_menu_nodes(role: MenuRole, app_name: &str, dev_menu: bool) -> Vec { + let item = |role: MenuRole| { + MenuNode::Item(MenuItemDef { + id: role_action_id(role).to_string(), + label: role.default_label().to_string(), + accelerator: default_accelerator(role).map(str::to_string), + enabled: true, + checked: false, + role: Some(role), + icon: None, + }) + }; + let items = |roles: &[MenuRole]| roles.iter().map(|role| item(*role)).collect(); + match role { + MenuRole::AppMenu => vec![ + MenuNode::Item(MenuItemDef { + id: role_action_id(MenuRole::About).to_string(), + label: format!("About {app_name}"), + accelerator: None, + enabled: true, + checked: false, + role: Some(MenuRole::About), + icon: None, + }), + MenuNode::Separator, + item(MenuRole::Hide), + item(MenuRole::HideOthers), + item(MenuRole::ShowAll), + MenuNode::Separator, + item(MenuRole::Quit), + ], + MenuRole::EditMenu => { + let mut nodes: Vec = items(&[MenuRole::Undo, MenuRole::Redo]); + nodes.push(MenuNode::Separator); + nodes.extend(items(&[ + MenuRole::Cut, + MenuRole::Copy, + MenuRole::Paste, + MenuRole::SelectAll, + ])); + nodes + } + MenuRole::WindowMenu => items(&[ + MenuRole::Minimize, + MenuRole::Zoom, + MenuRole::ToggleFullscreen, + MenuRole::Close, + ]), + MenuRole::HelpMenu => { + let mut nodes = Vec::new(); + if dev_menu { + nodes.push(MenuNode::Submenu(SubmenuDef { + id: Some("dev".to_string()), + label: "Developer".to_string(), + enabled: true, + icon: None, + items: [ + (crate::desktop::dev_menu::RELOAD, "Reload"), + (crate::desktop::dev_menu::OPEN_DEVTOOLS, "Open DevTools"), + ( + crate::desktop::dev_menu::ELEMENT_INSPECTOR, + "Toggle Element Inspector", + ), + ( + crate::desktop::dev_menu::PERF_MONITOR, + "Toggle Perf Monitor", + ), + (crate::desktop::dev_menu::FAST_REFRESH, "Fast Refresh"), + ] + .into_iter() + .map(|(id, label)| { + MenuNode::Item(MenuItemDef { + id: id.to_string(), + label: label.to_string(), + accelerator: None, + enabled: true, + checked: false, + role: None, + icon: None, + }) + }) + .collect(), + })); + } + nodes + } + other => vec![item(other)], + } +} + +pub struct DevMenuEnabled(pub bool); +impl gpui::Global for DevMenuEnabled {} + +fn dev_menu_enabled(cx: &App) -> bool { + cx.try_global::().is_some_and(|flag| flag.0) +} + +pub fn default_menu(app_name: &str) -> MenuModel { + let mut app_items = vec![ + MenuNode::Item(role_item(MenuRole::About, &format!("About {app_name}"))), + MenuNode::Separator, + ]; + if cfg!(target_os = "macos") { + app_items.push(MenuNode::Item(role_item(MenuRole::Services, "Services"))); + app_items.push(MenuNode::Separator); + } + app_items.extend([ + MenuNode::Item(role_item(MenuRole::Hide, &format!("Hide {app_name}"))), + MenuNode::Item(role_item(MenuRole::HideOthers, "Hide Others")), + MenuNode::Item(role_item(MenuRole::ShowAll, "Show All")), + MenuNode::Separator, + MenuNode::Item(role_item(MenuRole::Quit, &format!("Quit {app_name}"))), + ]); + + MenuModel { + items: vec![ + MenuNode::Submenu(menu_model::SubmenuDef { + id: None, + label: app_name.to_string(), + enabled: true, + items: app_items, + icon: None, + }), + MenuNode::StandardMenu(MenuRole::EditMenu), + MenuNode::StandardMenu(MenuRole::WindowMenu), + MenuNode::StandardMenu(MenuRole::HelpMenu), + ], + } +} + +fn role_item(role: MenuRole, label: &str) -> MenuItemDef { + MenuItemDef { + id: format!("role:{}", role_name(role)), + label: label.to_string(), + accelerator: default_accelerator(role).map(str::to_string), + enabled: role.is_supported(), + checked: false, + role: Some(role), + icon: None, + } +} + +pub fn default_accelerator(role: MenuRole) -> Option<&'static str> { + Some(match role { + MenuRole::Undo => "secondary-z", + MenuRole::Redo => "secondary-shift-z", + MenuRole::Cut => "secondary-x", + MenuRole::Copy => "secondary-c", + MenuRole::Paste => "secondary-v", + MenuRole::SelectAll => "secondary-a", + MenuRole::Minimize => "secondary-m", + MenuRole::Close => "secondary-w", + MenuRole::Quit => "secondary-q", + MenuRole::Hide => "secondary-h", + MenuRole::HideOthers => "secondary-alt-h", + MenuRole::ToggleFullscreen => "ctrl-cmd-f", + _ => return None, + }) +} + +fn apply(cx: &mut App, model: MenuModel, user_set: bool) -> Result<(), DesktopError> { + let app_name = app_name(cx); + let menus = build_menus(&model, &app_name, dev_menu_enabled(cx)); + let bindings = collect_bindings(dev_menu_enabled(cx), &model); + let accelerators = replace_accelerator_bindings(cx, &bindings); + // Windows has no `cx.set_menus()`; the model is attached to the HWND + // instead. Built twice because the menu builders are not + // cloneable. + #[cfg(target_os = "windows")] + { + let platform_menus = build_menus(&model, &app_name, dev_menu_enabled(cx)); + if let Some(handle) = crate::desktop::dev_menu::main_window(cx) { + let mut attached = None; + let _ = handle.update(cx, |_root: gpui::AnyView, window, _cx| { + attached = set_platform_menus(window, &platform_menus); + }); + if let Some((bar, hwnd)) = attached { + // Replacing the bar first detaches the previous window subclass, + // which still points into the old menu; only then is that menu + // dropped, so the window never calls through freed memory. + if let Some((previous, previous_hwnd)) = cx + .global_mut::() + .0 + .platform_menu + .replace((bar, hwnd)) + { + remove_platform_menu(previous, previous_hwnd); + } + } else { + log::warn!("[desktop] could not attach the Win32 menu bar"); + } + } + } + cx.set_menus(menus); + #[cfg(target_os = "macos")] + set_menu_bar_icons(&model); + let state = &mut cx.global_mut::().0; + state.model = model; + state.user_set = user_set; + state.bindings = bindings; + state.accelerators = accelerators; + state.rebuild_scheduled = false; + Ok(()) +} + +/// GPUI's menu items have no image, so the icons are set on the `NSMenu` it +/// just installed, matching items by title the way the model nests them. +#[cfg(target_os = "macos")] +fn set_menu_bar_icons(model: &MenuModel) { + use cocoa::base::{id, nil}; + use objc::{class, msg_send, sel, sel_impl}; + + const SIZE: f64 = 16.0; + + unsafe fn item_titled(menu: id, title: &str, from: &mut isize) -> id { + let count: isize = msg_send![menu, numberOfItems]; + while *from < count { + let item: id = msg_send![menu, itemAtIndex: *from]; + *from += 1; + let ns_title: id = msg_send![item, title]; + let utf8: *const std::os::raw::c_char = msg_send![ns_title, UTF8String]; + if !utf8.is_null() && std::ffi::CStr::from_ptr(utf8).to_str() == Ok(title) { + return item; + } + } + nil + } + + unsafe fn set_image(item: id, uri: &str) { + let Some(icon) = crate::desktop::icon::cached(uri) else { + return; + }; + let Some(image) = image::RgbaImage::from_raw(icon.width, icon.height, icon.rgba.clone()) + else { + return; + }; + let mut png = Vec::new(); + if image + .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) + .is_err() + { + return; + } + let data: id = msg_send![class!(NSData), dataWithBytes: png.as_ptr() length: png.len()]; + let ns_image: id = msg_send![class!(NSImage), alloc]; + let ns_image: id = msg_send![ns_image, initWithData: data]; + if ns_image.is_null() { + return; + } + let _: () = msg_send![ns_image, setSize: cocoa::foundation::NSSize::new(SIZE, SIZE)]; + let _: () = msg_send![item, setImage: ns_image]; + let _: () = msg_send![ns_image, release]; + } + + unsafe fn walk(menu: id, nodes: &[MenuNode]) { + if menu.is_null() { + return; + } + let mut from = 0; + for node in nodes { + match node { + MenuNode::Item(item) => { + let ns_item = item_titled(menu, &item.label, &mut from); + if let (false, Some(uri)) = (ns_item.is_null(), item.icon.as_deref()) { + if !item.checked { + set_image(ns_item, uri); + } + } + } + MenuNode::Submenu(submenu) => { + let ns_item = item_titled(menu, &submenu.label, &mut from); + if ns_item.is_null() { + continue; + } + if let Some(uri) = submenu.icon.as_deref() { + set_image(ns_item, uri); + } + let child: id = msg_send![ns_item, submenu]; + walk(child, &submenu.items); + } + MenuNode::Separator | MenuNode::StandardMenu(_) => {} + } + } + } + + // SAFETY: called on the main thread right after GPUI installed the menu, + // which it keeps alive as the application's main menu. + unsafe { + let app: id = msg_send![class!(NSApplication), sharedApplication]; + let main_menu: id = msg_send![app, mainMenu]; + walk(main_menu, &model.items); + } +} + +/// The titles of the menu bar's items that show an image. macOS only; the +/// test driver reads it, because the bar's images live in AppKit. +pub fn menu_bar_image_titles() -> Vec { + #[cfg(target_os = "macos")] + { + use cocoa::base::id; + use objc::{class, msg_send, sel, sel_impl}; + + unsafe fn walk(menu: id, out: &mut Vec) { + if menu.is_null() { + return; + } + let count: isize = msg_send![menu, numberOfItems]; + for index in 0..count { + let item: id = msg_send![menu, itemAtIndex: index]; + let image: id = msg_send![item, image]; + if !image.is_null() { + let title: id = msg_send![item, title]; + let utf8: *const std::os::raw::c_char = msg_send![title, UTF8String]; + if !utf8.is_null() { + out.push( + std::ffi::CStr::from_ptr(utf8) + .to_string_lossy() + .into_owned(), + ); + } + } + let submenu: id = msg_send![item, submenu]; + walk(submenu, out); + } + } + + let mut out = Vec::new(); + // SAFETY: read on the main thread from the application's main menu. + unsafe { + let app: id = msg_send![class!(NSApplication), sharedApplication]; + let main_menu: id = msg_send![app, mainMenu]; + walk(main_menu, &mut out); + } + out + } + #[cfg(not(target_os = "macos"))] + Vec::new() +} + +fn schedule_rebuild(cx: &mut App) { + { + let state = &mut cx.global_mut::().0; + if state.rebuild_scheduled { + return; + } + state.rebuild_scheduled = true; + } + let Some(window) = cx.active_window() else { + rebuild_now(cx); + return; + }; + let _ = window.update(cx, |_root, window, _cx| { + window.on_next_frame(move |_window, cx| { + let scheduled = cx + .try_global::() + .is_some_and(|state| state.0.rebuild_scheduled); + if scheduled { + rebuild_now(cx); + } + }); + }); +} + +fn rebuild_now(cx: &mut App) { + let model = current_model(cx); + let app_name = app_name(cx); + let user_set = cx.global::().0.user_set; + let _ = apply(cx, model, user_set); + let _ = app_name; +} + +fn collect_bindings(dev_menu: bool, model: &MenuModel) -> Vec<(String, String)> { + let mut bindings = model.accelerators(); + collect_role_bindings(&model.items, &mut bindings); + if dev_menu { + bindings.push(( + crate::desktop::dev_menu::ACCELERATOR_ID.to_string(), + crate::desktop::dev_menu::ACCELERATOR.to_string(), + )); + } + bindings +} + +fn collect_role_bindings(items: &[MenuNode], out: &mut Vec<(String, String)>) { + for node in items { + match node { + MenuNode::Item(item) => { + if let Some(role) = item.role { + if let Some(accelerator) = default_accelerator(role) { + out.push((item.id.clone(), accelerator.to_string())); + } + } + } + MenuNode::Submenu(submenu) => collect_role_bindings(&submenu.items, out), + MenuNode::StandardMenu(role) => { + for (id, accelerator) in standard_menu_bindings(*role) { + out.push((id.to_string(), accelerator.to_string())); + } + } + _ => {} + } + } +} + +fn standard_menu_bindings(role: MenuRole) -> Vec<(&'static str, &'static str)> { + let roles: &[MenuRole] = match role { + MenuRole::EditMenu => &[ + MenuRole::Undo, + MenuRole::Redo, + MenuRole::Cut, + MenuRole::Copy, + MenuRole::Paste, + MenuRole::SelectAll, + ], + MenuRole::WindowMenu => &[ + MenuRole::Minimize, + MenuRole::Zoom, + MenuRole::ToggleFullscreen, + MenuRole::Close, + ], + _ => &[], + }; + roles + .iter() + .filter_map(|role| { + let accelerator = default_accelerator(*role)?; + Some((role_action_id(*role), accelerator)) + }) + .collect() +} + +pub fn role_action_id(role: MenuRole) -> &'static str { + match role { + MenuRole::Undo => "role:undo", + MenuRole::Redo => "role:redo", + MenuRole::Cut => "role:cut", + MenuRole::Copy => "role:copy", + MenuRole::Paste => "role:paste", + MenuRole::SelectAll => "role:selectAll", + MenuRole::Minimize => "role:minimize", + MenuRole::Zoom => "role:zoom", + MenuRole::ToggleFullscreen => "role:toggleFullscreen", + MenuRole::Close => "role:close", + MenuRole::Quit => "role:quit", + MenuRole::Hide => "role:hide", + MenuRole::HideOthers => "role:hideOthers", + MenuRole::ShowAll => "role:showAll", + MenuRole::About => "role:about", + _ => "role:menu", + } +} + +fn is_desktop_action(action: &dyn Action) -> bool { + action.as_any().downcast_ref::().is_some() + || action.as_any().downcast_ref::().is_some() + || action.as_any().downcast_ref::().is_some() +} + +/// Windows: `muda` attaches a native Win32 menu to the window's HWND. +/// +/// Attaches or detaches the native menu bar on the window's HWND. +/// +/// `muda` 0.15 has no hide/show for an HWND, so this uses `SetMenu` directly and +/// keeps the `muda::Menu` alive in `AppMenuGlobal` either way: detaching is what +/// hiding means, and the same menu is re-attached to show it again. The window +/// subclass `muda` installed stays in place, so clicks still route back to the +/// same dispatch when the bar returns. +#[cfg(target_os = "windows")] +fn set_windows_menu_attached(cx: &mut gpui::App, window_id: u32, visible: bool) -> DesktopResult { + use muda::ContextMenu as _; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + use windows_sys::Win32::UI::WindowsAndMessaging::{DrawMenuBar, SetMenu}; + + let menu = if visible { + // Re-attach the menu that is still held in the global: hiding detached it + // rather than destroying it, so the same items come back. `hpopupmenu` is + // on muda's `ContextMenu` trait, not on `Menu` itself. + cx.try_global::() + .and_then(|global| global.0.platform_menu.as_ref()) + .map(|(menu, _)| menu.hpopupmenu() as isize) + .unwrap_or(0) + } else { + 0 + }; + crate::desktop::windows::with_window(cx, window_id, move |window| { + let Ok(handle) = HasWindowHandle::window_handle(window) else { + return Err(DesktopError::failed("window has no handle")); + }; + let RawWindowHandle::Win32(win32) = handle.as_raw() else { + return Err(DesktopError::failed("window handle is not Win32")); + }; + // SAFETY: the `HWND` comes from the window's raw handle and `menu` is the live popup menu this module owns. + unsafe { + SetMenu(win32.hwnd.get() as _, menu as _); + // The client size changed, so the frame has to be redrawn; GPUI picks + // the new size up from the `WM_SIZE` that follows. + DrawMenuBar(win32.hwnd.get() as _); + } + Ok(()) + }) +} + +/// The built `muda::Menu` is returned rather than dropped: installing it leaves +/// a window subclass whose `dwRefData` points into the menu, so dropping it here +/// would leave the window calling through freed memory on its next message. +#[cfg(target_os = "windows")] +fn set_platform_menus(window: &mut gpui::Window, menus: &[Menu]) -> Option<(muda::Menu, isize)> { + use super::context_menu::MudaAppend as _; + use muda::{ + CheckMenuItem, Menu as MudaMenu, MenuItem as MudaItem, PredefinedMenuItem, Submenu, + }; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + + // The id for a menu-bar item, so a click can be routed back to the action it + // stands for. `build_item` put the id in a `MenuAccelerator` action; that is + // the same value the accelerator table and the JS `menuPress` event use, so + // one id means one action however the user reached it. + fn action_id(action: &dyn gpui::Action) -> Option { + action + .as_any() + .downcast_ref::() + .map(|accelerator| accelerator.id.clone()) + } + + fn build(menu: &Menu, items: &mut Vec>) { + for item in &menu.items { + match item { + MenuItem::Separator => items.push(Box::new(PredefinedMenuItem::separator())), + MenuItem::Submenu(submenu) => { + let child = Submenu::new(submenu.name.to_string(), !submenu.disabled); + let mut child_items: Vec> = Vec::new(); + build(submenu, &mut child_items); + let refs: Vec<&dyn muda::IsMenuItem> = + child_items.iter().map(|item| item.as_ref()).collect(); + let _ = child.append_all(&refs); + items.push(Box::new(child)); + } + MenuItem::Action { + name, + action, + checked, + disabled, + .. + } => { + // The id travels as the muda item's id, which is what a + // `MenuEvent` reports. Without it a click on the menu bar is + // indistinguishable from any other item's, which is why + // nothing happened before. + // + // A checked item must be a `CheckMenuItem` in muda; a plain + // one has no `set_checked`. `disabled` is honoured for both: + // a checked-but-disabled item used to be enabled regardless. + let id = action_id(action.as_ref()); + match (*checked, id) { + (true, Some(id)) => items.push(Box::new(CheckMenuItem::with_id( + id, + name.to_string(), + !*disabled, + true, + None, + ))), + (true, None) => items.push(Box::new(CheckMenuItem::new( + name.to_string(), + !*disabled, + true, + None, + ))), + (false, Some(id)) => items.push(Box::new(MudaItem::with_id( + id, + name.to_string(), + !*disabled, + None, + ))), + (false, None) => { + items.push(Box::new(MudaItem::new(name.to_string(), !*disabled, None))) + } + } + } + // The Services menu is a macOS concept; there is no Windows + // equivalent to attach to the menu bar. + MenuItem::SystemMenu(_) => {} + } + } + } + + let handle = match HasWindowHandle::window_handle(window) { + Ok(handle) => handle, + Err(_) => return None, + }; + let hwnd = match handle.as_raw() { + RawWindowHandle::Win32(win32) => win32.hwnd.get() as isize, + _ => return None, + }; + let mut bar = MudaMenu::new(); + let mut bar_items: Vec> = Vec::new(); + for menu in menus { + let submenu = Submenu::new(menu.name.to_string(), !menu.disabled); + let mut menu_items: Vec> = Vec::new(); + build(menu, &mut menu_items); + let refs: Vec<&dyn muda::IsMenuItem> = + menu_items.iter().map(|item| item.as_ref()).collect(); + let _ = submenu.append_all(&refs); + bar_items.push(Box::new(submenu)); + } + let refs: Vec<&dyn muda::IsMenuItem> = bar_items.iter().map(|item| item.as_ref()).collect(); + let _ = bar.append_all(&refs); + // SAFETY: `hwnd` is the live window handle this menu bar is built for. + if unsafe { bar.init_for_hwnd(hwnd) }.is_err() { + return None; + } + // Route menu-bar clicks to the same dispatch an accelerator uses, so one id + // means one action however the user reached it. + // + // Registration goes through the app's single `muda` event route + // (`desktop::menu_events`), not a private handler: `muda` exposes one event + // channel and its handler setter is first-caller-wins, so a handler per menu + // system would silently starve the others. The predicate accepts everything, + // so it is registered last and acts as the fallback — a menu-bar id is + // authoritative here, and `trigger_item` already forwards an id it does not + // recognise to JS as a `menuPress` event. + crate::desktop::menu_events::register(Box::new(|id| { + if let Some(handler) = rn_gpui_sys::MENU_BAR_PERFORM_HANDLER.get() { + handler(id.to_string()); + return true; + } + false + })); + Some((bar, hwnd)) +} + +/// Windows: detaches an attached menu bar and drops it. +/// +/// `muda::Menu::remove_for_hwnd` clears the Win32 menu but leaves the window +/// subclass it installed in place; that subclass holds a raw pointer into the +/// menu. It has to be detached explicitly before the menu is dropped, otherwise +/// the next message the window receives calls into freed memory. +#[cfg(target_os = "windows")] +fn remove_platform_menu(mut menu: muda::Menu, hwnd: isize) { + use muda::ContextMenu as _; + // SAFETY: `hwnd` is the window the menu was attached to, and it is detached once. + unsafe { + menu.detach_menu_subclass_from_hwnd(hwnd); + let _ = menu.remove_for_hwnd(hwnd); + } +} + +fn replace_accelerator_bindings( + cx: &mut App, + bindings: &[(String, String)], +) -> Vec<(gpui::Keystroke, AcceleratorAction)> { + let keymap = cx.key_bindings(); + let kept: Vec = keymap + .borrow() + .bindings() + .filter(|binding| !is_desktop_action(binding.action())) + .cloned() + .collect(); + { + let mut keymap = keymap.borrow_mut(); + keymap.clear(); + keymap.add_bindings(kept); + } + + let mut new_bindings: Vec = Vec::with_capacity(bindings.len()); + let mut accelerators: Vec<(gpui::Keystroke, AcceleratorAction)> = Vec::new(); + for (id, keystroke) in bindings { + if keystroke.is_empty() { + continue; + } + let action = accelerator_action(id); + new_bindings.push(binding_for(keystroke, &action)); + if let Ok(parsed) = gpui::Keystroke::parse(keystroke) { + accelerators.push((parsed, action)); + } + } + cx.bind_keys(new_bindings); + accelerators +} + +fn accelerator_action(id: &str) -> AcceleratorAction { + if id == crate::desktop::dev_menu::ACCELERATOR_ID { + return AcceleratorAction::Host("devMenu".to_string()); + } + if crate::desktop::dev_menu::is_command(id) { + return AcceleratorAction::Dev(id.to_string()); + } + if let Some(kind) = id.strip_prefix("role:") { + if matches!( + kind, + "undo" | "redo" | "cut" | "copy" | "paste" | "selectAll" + ) { + return AcceleratorAction::Edit(kind.to_string()); + } + if matches!( + kind, + "quit" + | "hide" + | "hideOthers" + | "showAll" + | "minimize" + | "zoom" + | "toggleFullscreen" + | "close" + | "about" + ) { + return AcceleratorAction::Host(kind.to_string()); + } + } + AcceleratorAction::Press(id.to_string()) +} + +/// A menu accelerator, as a keymap binding in the app-wide context. +/// +/// The context matters. A shortcut belongs to a `ShortcutScope`, so its binding +/// matches at a greater depth than anything in the window root's `rn-app`; GPUI +/// dispatches the bindings for one keystroke in precedence order, so a scoped +/// shortcut consumes the key first and the menu item never runs. Without a +/// context the accelerator would tie with the window scope and win by being +/// added later, which is the behaviour this work set out to fix. +fn binding_for(keystroke: &str, action: &AcceleratorAction) -> KeyBinding { + let context = crate::desktop::keymap::APP_CONTEXT; + match action { + AcceleratorAction::Press(id) | AcceleratorAction::Dev(id) => { + KeyBinding::new(keystroke, MenuAccelerator { id: id.clone() }, Some(context)) + } + AcceleratorAction::Edit(kind) => KeyBinding::new( + keystroke, + MenuEditAction { kind: kind.clone() }, + Some(context), + ), + AcceleratorAction::Host(kind) => KeyBinding::new( + keystroke, + MenuHostAction { kind: kind.clone() }, + Some(context), + ), + } +} + +fn build_menus(model: &MenuModel, app_name: &str, dev_menu: bool) -> Vec { + model + .items + .iter() + .filter_map(|node| match node { + MenuNode::StandardMenu(role) => Some(standard_menu(*role, app_name, dev_menu)), + MenuNode::Submenu(submenu) => Some( + Menu::new(SharedString::from(submenu.label.clone())) + .items(build_items(&submenu.items)) + .disabled(!submenu.enabled), + ), + MenuNode::Item(item) => Some( + Menu::new(SharedString::from(item.label.clone())).items(vec![build_item(item)]), + ), + MenuNode::Separator => None, + }) + .collect() +} + +fn standard_menu(role: MenuRole, app_name: &str, dev_menu: bool) -> Menu { + let (name, items): (&str, Vec) = match role { + MenuRole::EditMenu => ( + "Edit", + vec![ + role_menu_item(MenuRole::Undo, None), + role_menu_item(MenuRole::Redo, None), + MenuItem::separator(), + role_menu_item(MenuRole::Cut, None), + role_menu_item(MenuRole::Copy, None), + role_menu_item(MenuRole::Paste, None), + role_menu_item(MenuRole::SelectAll, None), + ], + ), + MenuRole::WindowMenu => ( + "Window", + vec![ + role_menu_item(MenuRole::Minimize, None), + role_menu_item(MenuRole::Zoom, None), + MenuItem::separator(), + role_menu_item(MenuRole::ToggleFullscreen, None), + MenuItem::separator(), + role_menu_item(MenuRole::Close, None), + ], + ), + MenuRole::HelpMenu => ( + "Help", + if dev_menu { + vec![MenuItem::submenu( + Menu::new(SharedString::from("Developer")).items(dev_menu_items()), + )] + } else { + Vec::new() + }, + ), + MenuRole::AppMenu => ( + app_name, + vec![role_menu_item(MenuRole::About, Some(app_name))], + ), + _ => ("Menu", Vec::new()), + }; + Menu::new(SharedString::from(name)).items(items) +} + +fn build_items(items: &[MenuNode]) -> Vec { + items + .iter() + .filter_map(|node| match node { + MenuNode::Separator => Some(MenuItem::separator()), + MenuNode::StandardMenu(role) => Some(MenuItem::submenu(standard_menu( + *role, + role.default_label(), + false, + ))), + MenuNode::Submenu(submenu) => Some( + MenuItem::submenu( + Menu::new(SharedString::from(submenu.label.clone())) + .items(build_items(&submenu.items)), + ) + .disabled(!submenu.enabled), + ), + MenuNode::Item(item) => Some(build_item(item)), + }) + .collect() +} + +fn dev_menu_items() -> Vec { + use crate::desktop::dev_menu as dev; + [ + (dev::RELOAD, "Reload", false), + (dev::OPEN_DEVTOOLS, "Open DevTools", false), + (dev::ELEMENT_INSPECTOR, "Toggle Element Inspector", false), + (dev::PERF_MONITOR, "Toggle Perf Monitor", false), + (dev::FAST_REFRESH, "Fast Refresh", false), + ] + .into_iter() + .map(|(id, label, checked)| { + MenuItem::action( + SharedString::from(label), + MenuAccelerator { id: id.to_string() }, + ) + .checked(checked) + }) + .collect() +} + +fn build_item(item: &MenuItemDef) -> MenuItem { + if let Some(role) = item.role { + let mut built = role_menu_item(role, Some(&item.label)); + built = built.checked(item.checked); + return built.disabled(!item.enabled); + } + let mut built = MenuItem::action( + SharedString::from(item.label.clone()), + MenuAccelerator { + id: item.id.clone(), + }, + ); + built = built.checked(item.checked).disabled(!item.enabled); + built +} + +fn role_menu_item(role: MenuRole, label: Option<&str>) -> MenuItem { + let label = label.unwrap_or_else(|| role.default_label()); + match role { + MenuRole::Services => { + MenuItem::os_submenu(SharedString::from(label), SystemMenuType::Services) + } + MenuRole::Cut + | MenuRole::Copy + | MenuRole::Paste + | MenuRole::SelectAll + | MenuRole::Undo + | MenuRole::Redo => MenuItem::action( + SharedString::from(label), + MenuEditAction { + kind: role_action_id(role).trim_start_matches("role:").to_string(), + }, + ), + MenuRole::About + | MenuRole::Hide + | MenuRole::HideOthers + | MenuRole::ShowAll + | MenuRole::Quit + | MenuRole::Minimize + | MenuRole::Zoom + | MenuRole::ToggleFullscreen + | MenuRole::Close => MenuItem::action( + SharedString::from(label), + MenuHostAction { + kind: role_action_id(role).trim_start_matches("role:").to_string(), + }, + ), + MenuRole::Help + | MenuRole::AppMenu + | MenuRole::EditMenu + | MenuRole::WindowMenu + | MenuRole::HelpMenu => MenuItem::action( + SharedString::from(label), + MenuHostAction { + kind: "noop".to_string(), + }, + ), + } +} + +fn emit_menu_press(cx: &mut App, id: &str) { + let Some(store) = store_from(cx) else { + return; + }; + let host = store.read(cx).host.clone(); + let window_id = active_window_key(cx).unwrap_or(1); + host.emit_device_event_json( + "gpuiDesktop:menuPress", + &json!({"id": id, "windowId": window_id}).to_string(), + ); +} + +fn active_window_key(cx: &App) -> Option { + let handle = cx.active_window()?; + let windows = cx.try_global::()?; + windows + .0 + .keys() + .into_iter() + .find(|key| windows.0.get(*key).map(|entry| entry.handle) == Some(handle)) +} + +/// Must not be called from inside a window update: a nested `update_window` +/// fails. +fn focused_text_input(cx: &mut App, window: gpui::AnyWindowHandle) -> Option> { + let debug = std::env::var_os("RN_GPUI_DEBUG_DESKTOP").is_some(); + let result = window.update(cx, |_root, window, cx| { + let focused = window.focused(cx)?; + let store = store_from(cx)?; + let node = { + let store = store.read(cx); + store + .nodes + .values() + .find(|node| node.read(cx).nearest_focus_handle(cx).as_ref() == Some(&focused)) + .cloned() + }; + if debug { + log::info!( + "[desktop][menu] focused node found={} (store nodes={})", + node.is_some(), + store.read(cx).nodes.len() + ); + } + node + }); + if let Err(error) = &result { + if debug { + log::info!("[desktop][menu] focus lookup failed: {error:?}"); + } + } + result.ok().flatten() +} + +/// Runs a standard menu role, as a menu item or accelerator would. +/// +/// A custom menu drawn in React is ordinary views, so nothing native performs its +/// role items. This is the one entry point that does, which is why it exists: +/// without it a custom "Copy" row would look right and do nothing. +/// +/// It goes through the same dispatch the native menu uses, so the action, the +/// window and the effect are identical however the user asked for it. +pub fn perform_role(cx: &mut App, role: &str) -> DesktopResult { + let handle = cx.active_window(); + match role { + "undo" => perform_edit(cx, "undo", handle), + "redo" => perform_edit(cx, "redo", handle), + "cut" => perform_edit(cx, "cut", handle), + "copy" => perform_edit(cx, "copy", handle), + "paste" => perform_edit(cx, "paste", handle), + "selectAll" => perform_edit(cx, "selectAll", handle), + "minimize" => { + if let Some(handle) = handle { + let _ = handle.update(cx, |_, window, _| window.minimize_window()); + } + } + "zoom" => { + if let Some(handle) = handle { + let _ = handle.update(cx, |_, window, _| window.zoom_window()); + } + } + "toggleFullscreen" => { + if let Some(handle) = handle { + let _ = handle.update(cx, |_, window, _| window.toggle_fullscreen()); + } + } + "close" => { + if let Some(handle) = handle { + let _ = handle.update(cx, |_, window, _| window.remove_window()); + } + } + "quit" | "hide" | "hideOthers" | "showAll" | "about" => { + perform_host_action(cx, role, handle) + } + // A menu with no action of its own: nothing to do, and saying so is + // better than silently pretending. + other => { + return Err(DesktopError::failed(format!( + "{other} is not a role this platform can perform" + ))) + } + } + Ok(json!({"role": role})) +} + +fn perform_edit(cx: &mut App, kind: &str, handle: Option) { + let kind = kind.to_string(); + // Deferred: the action is dispatched from inside a window update, and GPUI + // forbids re-entering the same window. + cx.defer(move |cx| { + let debug = std::env::var_os("RN_GPUI_DEBUG_DESKTOP").is_some(); + let Some(window) = handle.or_else(|| cx.active_window()) else { + return; + }; + let Some(node) = focused_text_input(cx, window) else { + if debug { + log::info!("[desktop][menu] edit {kind}: no focused text input"); + } + return; + }; + let _ = window.update(cx, move |_root, window, cx| { + node.update(cx, |node, cx| match kind.as_str() { + "copy" => { + crate::builtins::textinput::input::copy_selection(node, cx); + } + "cut" => { + crate::builtins::textinput::input::cut_selection(node, window, cx); + } + "paste" => { + crate::builtins::textinput::input::paste_clipboard(node, window, cx); + } + "selectAll" => { + crate::builtins::textinput::input::select_all(node, window, cx); + } + // TODO: undo/redo need an edit history in the GPUI text input. The + // item is present so an app can handle the press itself. + _ => {} + }); + if debug { + let node = node.read(cx); + log::info!( + "[desktop][menu] edit {kind} on {} selection={:?}", + node.component, + node.input_selection + ); + } + }); + }); +} + +/// Deferred: a menu action runs inside the platform's menu callback, and a +/// reload or a perf-monitor window must not start from there. +fn perform_dev_action(cx: &mut App, id: String) { + cx.defer(move |cx| crate::desktop::dev_menu::perform(cx, &id)); +} + +fn perform_host_action(cx: &mut App, kind: &str, handle: Option) { + match kind { + "quit" => cx.quit(), + "hide" => cx.hide(), + "hideOthers" => cx.hide_other_apps(), + "showAll" => cx.unhide_other_apps(), + "about" => show_about_panel(cx), + // Deferred: the accelerator runs inside the keystroke's window update. + "devMenu" => cx.defer(|cx| crate::desktop::dev_menu::show(cx)), + "minimize" | "zoom" | "toggleFullscreen" | "close" => { + let kind = kind.to_string(); + cx.defer(move |cx| { + let Some(window) = handle.or_else(|| cx.active_window()) else { + return; + }; + let _ = window.update(cx, move |_root, window, _cx| match kind.as_str() { + "minimize" => window.minimize_window(), + "zoom" => window.zoom_window(), + "toggleFullscreen" => window.toggle_fullscreen(), + "close" => window.remove_window(), + _ => {} + }); + }); + } + _ => {} + } +} + +#[cfg(target_os = "macos")] +fn show_about_panel(_cx: &mut App) { + use objc::{class, msg_send, sel, sel_impl}; + // SAFETY: `NSApplication` is messaged from the main thread. + unsafe { + let app: cocoa::base::id = msg_send![class!(NSApplication), sharedApplication]; + let _: () = + msg_send![app, orderFrontStandardAboutPanel: std::ptr::null::()]; + } +} + +#[cfg(not(target_os = "macos"))] +fn show_about_panel(_cx: &mut App) {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_menu_has_the_standard_groups() { + let model = default_menu("Tester"); + assert!(model.has_help_menu()); + assert!(model.find("role:quit").is_some()); + let bindings = collect_bindings(false, &model); + assert!(bindings + .iter() + .any(|(id, key)| id == "role:copy" && key == "secondary-c")); + assert!(bindings + .iter() + .any(|(id, key)| id == "role:quit" && key == "secondary-q")); + } + + #[test] + fn standard_edit_menu_installs_accelerators() { + let model = MenuModel { + items: vec![MenuNode::StandardMenu(MenuRole::EditMenu)], + }; + let bindings = collect_bindings(false, &model); + assert_eq!(bindings.len(), 6); + assert!(bindings.iter().any(|(_, key)| key == "secondary-v")); + } + + #[test] + fn unsupported_roles_are_dropped_from_the_built_menu() { + let model = default_menu("Tester"); + let menus = build_menus(&model, "Tester", false); + assert!(!menus.is_empty()); + } + + #[test] + fn developer_items_run_on_the_host() { + use crate::desktop::dev_menu as dev; + for id in [ + dev::RELOAD, + dev::OPEN_DEVTOOLS, + dev::ELEMENT_INSPECTOR, + dev::PERF_MONITOR, + dev::FAST_REFRESH, + ] { + assert_eq!(accelerator_action(id), AcceleratorAction::Dev(id.into())); + } + assert_eq!( + accelerator_action(dev::ACCELERATOR_ID), + AcceleratorAction::Host("devMenu".into()) + ); + assert_eq!( + accelerator_action("dev:custom"), + AcceleratorAction::Press("dev:custom".into()) + ); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/capabilities.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/capabilities.rs new file mode 100644 index 0000000000..a3260a3402 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/capabilities.rs @@ -0,0 +1,213 @@ +#[cfg(target_os = "linux")] +use std::sync::atomic::{AtomicU8, Ordering}; + +pub const fn native_app_menu() -> bool { + cfg!(any(target_os = "macos", target_os = "windows")) +} + +pub const fn native_context_menu() -> bool { + cfg!(any(target_os = "macos", target_os = "windows")) +} + +pub const fn tray() -> bool { + cfg!(any( + target_os = "macos", + target_os = "windows", + target_os = "linux" + )) +} + +/// Whether the tray popover can be placed at the icon, rather than wherever the +/// compositor decides. +/// +/// macOS places it natively. Linux is decided at run time: X11 can position a +/// window (`_NET_WM_WINDOW_TYPE_UTILITY` + `USPosition`), Wayland cannot. The +/// Wayland layer-shell path (KDE, wlroots, COSMIC) is not implemented yet; when +/// it lands this becomes true there too, and stays false on GNOME, which has no +/// layer-shell. Windows has no anchored popover. +pub fn tray_popover_anchored() -> bool { + #[cfg(target_os = "macos")] + { + true + } + #[cfg(target_os = "linux")] + { + !is_wayland() + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + false + } +} + +/// Wayland has no absolute window positioning and no raise-above. +pub fn is_wayland() -> bool { + #[cfg(target_os = "linux")] + { + std::env::var_os("WAYLAND_DISPLAY").is_some() + } + #[cfg(not(target_os = "linux"))] + { + false + } +} + +pub fn window_position() -> bool { + !is_wayland() +} + +pub fn always_on_top() -> bool { + !is_wayland() +} + +/// Whether a window can have its title bar removed while keeping the traffic +/// lights or the app's own caption buttons. +/// +/// macOS and Windows always can. Linux cannot on its own: the title bar belongs +/// to the window manager, and the only way to remove it is client-side +/// decorations, so the answer follows [`client_decorations`] there. Anywhere +/// else the shell does not run at all, so the answer is false rather than a +/// promise nothing can keep. +pub fn transparent_titlebar() -> bool { + #[cfg(target_os = "linux")] + { + client_decorations() + } + #[cfg(not(target_os = "linux"))] + { + cfg!(any(target_os = "macos", target_os = "windows")) + } +} + +/// Whether a frameless window can be frameless *and* still resized. +/// +/// macOS and Windows keep a native resize border around a title-bar-less window, +/// so a custom title bar needs nothing more from them. Linux has to ask the +/// compositor for client-side decorations, and only some window managers and +/// compositors grant that: X11 needs a compositor that advertises +/// `_GTK_FRAME_EXTENTS`, Wayland needs `zxdg_decoration_manager_v1`. Where the +/// request is refused the window keeps its server-side frame — and its title bar +/// — so an app must draw caption buttons only where this is true, or it draws a +/// second set beside the window manager's own. +/// +/// On Linux the answer comes from the window itself rather than a guess: the +/// bootstrap records what `window.window_decorations()` reported once the main +/// window exists, which is the only authority on whether the request was +/// granted. Before that the answer is "unknown", and the safe answer to an +/// unknown is false. +pub fn client_decorations() -> bool { + #[cfg(target_os = "linux")] + { + match OBSERVED_CLIENT_DECORATIONS.load(Ordering::Relaxed) { + DECORATIONS_CLIENT => true, + DECORATIONS_SERVER => false, + // A Wayland window starts on client-side decorations and only + // switches when the compositor insists, so before the first window + // exists that is the better guess. + _ => is_wayland(), + } + } + #[cfg(not(target_os = "linux"))] + { + // The same platforms `transparent_titlebar` is true on: a window only + // exists there, and both keep a native resize border when the caption is + // removed. + cfg!(any(target_os = "macos", target_os = "windows")) + } +} + +#[cfg(target_os = "linux")] +const DECORATIONS_UNKNOWN: u8 = 0; +#[cfg(target_os = "linux")] +const DECORATIONS_SERVER: u8 = 1; +#[cfg(target_os = "linux")] +const DECORATIONS_CLIENT: u8 = 2; + +/// What the compositor gave the windows opened so far. +/// +/// An atomic rather than a `OnceLock` because a second window can be refused +/// after the first was granted, and the later answer is the one that matters: +/// the caption buttons are drawn per window, but the capability is read once. +#[cfg(target_os = "linux")] +static OBSERVED_CLIENT_DECORATIONS: AtomicU8 = AtomicU8::new(DECORATIONS_UNKNOWN); + +/// Records whether a window ended up with client-side decorations. +#[cfg(target_os = "linux")] +pub fn note_window_decorations(decorations: gpui::Decorations) { + let value = match decorations { + gpui::Decorations::Client { .. } => DECORATIONS_CLIENT, + gpui::Decorations::Server => DECORATIONS_SERVER, + }; + OBSERVED_CLIENT_DECORATIONS.store(value, Ordering::Relaxed); +} + +/// Nothing to record: these platforms always have a native border to resize by. +#[cfg(not(target_os = "linux"))] +pub fn note_window_decorations(_decorations: gpui::Decorations) {} + +/// Whether the menu bar is drawn inside the window, so hiding it does something. +/// +/// macOS is false: its menu bar is the system's, at the top of the screen, and +/// nothing an app does can remove it. Windows and Linux draw theirs inside the +/// window, which is why `Menu.setMenuBarVisible` only means something there. +pub const fn menu_bar_in_window() -> bool { + cfg!(any(target_os = "windows", target_os = "linux")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn macos_supports_native_menus_and_anchored_popovers() { + if cfg!(target_os = "macos") { + assert!(native_app_menu()); + assert!(native_context_menu()); + assert!(tray()); + assert!(tray_popover_anchored()); + assert!(transparent_titlebar()); + assert!(!menu_bar_in_window()); + } + } + + #[test] + fn the_menu_bar_is_only_in_window_where_it_can_be_hidden() { + if cfg!(any(target_os = "windows", target_os = "linux")) { + assert!(menu_bar_in_window()); + } + } + + #[test] + fn a_custom_title_bar_is_resizable_off_linux_without_asking_the_compositor() { + // Windows and macOS keep their native resize border when the caption is + // removed, so the capability is unconditional there and `TitleBar` can + // draw caption buttons on any window it is put in. + if cfg!(any(target_os = "macos", target_os = "windows")) { + assert!(client_decorations()); + } + } + + #[cfg(target_os = "linux")] + #[test] + fn linux_reports_what_the_compositor_actually_granted() { + // The bootstrap records the answer per window; until it does, an X11 + // session has to assume the request will be refused, because guessing + // "granted" would draw caption buttons next to the window manager's own. + note_window_decorations(gpui::Decorations::Server); + assert!(!client_decorations()); + + note_window_decorations(gpui::Decorations::Client { + tiling: gpui::Tiling::default(), + }); + assert!(client_decorations()); + + // A later window that was refused wins: the capability must describe + // what the newest window actually looks like. + note_window_decorations(gpui::Decorations::Server); + assert!(!client_decorations()); + // A transparent title bar is the same question on Linux: without + // client-side decorations the window manager keeps its own bar, and the + // app cannot remove it. + assert!(!transparent_titlebar()); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/client_frame.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/client_frame.rs new file mode 100644 index 0000000000..0c9711c4a7 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/client_frame.rs @@ -0,0 +1,160 @@ +//! The invisible border that makes a frameless window resizable. +//! +//! A window with client-side decorations has no frame for the user to grab, so +//! the app draws one: eight invisible strips along and around the edges, each +//! starting a native resize when pressed. Only Linux needs this — macOS and +//! Windows keep a native resize border around a title-bar-less window — so the +//! element is only built there. The geometry and the cursor are +//! platform-independent and unit-tested everywhere, which is why the module is +//! not itself behind a `cfg`: the tests are what keep the numbers right, and they +//! have to run on the machine the change is made on. +#![allow(dead_code)] + +use gpui::{px, Bounds, CursorStyle, Pixels, Point, ResizeEdge, Size}; + +/// How thick the grab area is, in points. +/// +/// Eight is what GTK and winit use for a client-decorated edge: thinner and the +/// edge is hard to hit, thicker and it starts eating presses meant for content. +pub const BORDER: f32 = 8.0; + +/// Every edge, in the order the border draws them. +/// +/// The four sides come first and the corners last: a later child paints in +/// front, so where a corner overlaps the two sides it belongs to, the corner is +/// what the pointer hits. +pub const EDGES: [ResizeEdge; 8] = [ + ResizeEdge::Top, + ResizeEdge::Bottom, + ResizeEdge::Left, + ResizeEdge::Right, + ResizeEdge::TopLeft, + ResizeEdge::TopRight, + ResizeEdge::BottomLeft, + ResizeEdge::BottomRight, +]; + +/// Where one edge's grab area sits inside a window of this size. +/// +/// The thickness shrinks with a window smaller than two borders, so a tiny +/// window still lays out: two overlapping 8px strips that each claim half would +/// leave the middle unfindable. +pub fn border_bounds(edge: ResizeEdge, width: f32, height: f32) -> Bounds { + let width = width.max(0.0); + let height = height.max(0.0); + let thickness = BORDER.min(width / 2.0).min(height / 2.0); + let (x, y, w, h) = match edge { + ResizeEdge::Top => (0.0, 0.0, width, thickness), + ResizeEdge::Bottom => (0.0, height - thickness, width, thickness), + ResizeEdge::Left => (0.0, 0.0, thickness, height), + ResizeEdge::Right => (width - thickness, 0.0, thickness, height), + ResizeEdge::TopLeft => (0.0, 0.0, thickness, thickness), + ResizeEdge::TopRight => (width - thickness, 0.0, thickness, thickness), + ResizeEdge::BottomLeft => (0.0, height - thickness, thickness, thickness), + ResizeEdge::BottomRight => (width - thickness, height - thickness, thickness, thickness), + }; + Bounds::new(Point::new(px(x), px(y)), Size::new(px(w), px(h))) +} + +/// The cursor a resize edge shows, in the CSS spelling each edge means. +pub fn edge_cursor(edge: ResizeEdge) -> CursorStyle { + match edge { + ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown, + ResizeEdge::Left | ResizeEdge::Right => CursorStyle::ResizeLeftRight, + // `nwse-resize` runs from the top-left corner to the bottom-right one. + ResizeEdge::TopLeft | ResizeEdge::BottomRight => CursorStyle::ResizeUpRightDownLeft, + ResizeEdge::TopRight | ResizeEdge::BottomLeft => CursorStyle::ResizeUpLeftDownRight, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn rect(edge: ResizeEdge, width: f32, height: f32) -> (f32, f32, f32, f32) { + let bounds = border_bounds(edge, width, height); + ( + f32::from(bounds.origin.x), + f32::from(bounds.origin.y), + f32::from(bounds.size.width), + f32::from(bounds.size.height), + ) + } + + #[test] + fn each_side_spans_the_window_and_sits_against_its_own_edge() { + assert_eq!(rect(ResizeEdge::Top, 400.0, 300.0), (0.0, 0.0, 400.0, 8.0)); + assert_eq!( + rect(ResizeEdge::Bottom, 400.0, 300.0), + (0.0, 292.0, 400.0, 8.0) + ); + assert_eq!(rect(ResizeEdge::Left, 400.0, 300.0), (0.0, 0.0, 8.0, 300.0)); + assert_eq!( + rect(ResizeEdge::Right, 400.0, 300.0), + (392.0, 0.0, 8.0, 300.0) + ); + } + + #[test] + fn a_corner_is_a_square_in_its_own_corner() { + assert_eq!( + rect(ResizeEdge::TopLeft, 400.0, 300.0), + (0.0, 0.0, 8.0, 8.0) + ); + assert_eq!( + rect(ResizeEdge::TopRight, 400.0, 300.0), + (392.0, 0.0, 8.0, 8.0) + ); + assert_eq!( + rect(ResizeEdge::BottomLeft, 400.0, 300.0), + (0.0, 292.0, 8.0, 8.0) + ); + assert_eq!( + rect(ResizeEdge::BottomRight, 400.0, 300.0), + (392.0, 292.0, 8.0, 8.0) + ); + } + + #[test] + fn the_border_shrinks_to_fit_a_tiny_window() { + // A window narrower than two borders would otherwise place the right + // strip at a negative x, outside the window it is meant to resize. + let (x, _, w, _) = rect(ResizeEdge::Right, 10.0, 10.0); + assert_eq!((x, w), (5.0, 5.0)); + let (x, y, w, h) = rect(ResizeEdge::BottomRight, 10.0, 6.0); + assert_eq!((x, y, w, h), (7.0, 3.0, 3.0, 3.0)); + } + + #[test] + fn an_empty_window_has_no_border() { + assert_eq!(rect(ResizeEdge::Right, 0.0, 0.0), (0.0, 0.0, 0.0, 0.0)); + assert_eq!(rect(ResizeEdge::Top, -5.0, 20.0), (0.0, 0.0, 0.0, 0.0)); + } + + #[test] + fn every_edge_has_the_cursor_that_matches_its_direction() { + assert_eq!(edge_cursor(ResizeEdge::Top), CursorStyle::ResizeUpDown); + assert_eq!(edge_cursor(ResizeEdge::Bottom), CursorStyle::ResizeUpDown); + assert_eq!(edge_cursor(ResizeEdge::Left), CursorStyle::ResizeLeftRight); + assert_eq!(edge_cursor(ResizeEdge::Right), CursorStyle::ResizeLeftRight); + // The two diagonals must not be swapped: a browser and a window manager + // disagree about nothing else here, and a wrong cursor is the visible + // symptom of a wrong resize direction. + assert_eq!( + edge_cursor(ResizeEdge::TopLeft), + CursorStyle::ResizeUpRightDownLeft + ); + assert_eq!( + edge_cursor(ResizeEdge::BottomRight), + CursorStyle::ResizeUpRightDownLeft + ); + assert_eq!( + edge_cursor(ResizeEdge::TopRight), + CursorStyle::ResizeUpLeftDownRight + ); + assert_eq!( + edge_cursor(ResizeEdge::BottomLeft), + CursorStyle::ResizeUpLeftDownRight + ); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/context_menu.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/context_menu.rs new file mode 100644 index 0000000000..ddca01cdd0 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/context_menu.rs @@ -0,0 +1,515 @@ +use crate::desktop::errors::DesktopError; +use crate::desktop::menu_model::{self, MenuModel, MenuNode}; +use crate::desktop::windows::DesktopWindowsGlobal; +use gpui::{AnyWindowHandle, App}; +use serde_json::{json, Value}; +use std::cell::RefCell; +use std::rc::Rc; + +pub struct ContextMenuRequest { + pub window: AnyWindowHandle, + pub x: f32, + pub y: f32, + pub model: MenuModel, + pub native_view: isize, +} + +/// Settles one context menu: the module's promise, or a caller's callback. +#[derive(Clone)] +pub enum Responder { + Desktop { reply: Rc>> }, + Callback(Rc)>>>>), +} + +/// Answers the promise a `contextMenuShow` call is waiting on. +pub type Reply = futures::channel::oneshot::Sender>; + +impl Responder { + pub fn desktop(reply: Reply) -> Self { + Self::Desktop { + reply: Rc::new(RefCell::new(Some(reply))), + } + } + + pub fn callback(callback: Box)>) -> Self { + Self::Callback(Rc::new(RefCell::new(Some(callback)))) + } + + pub fn resolve(&self, selection: Option<&str>) { + match self { + Self::Desktop { reply } => { + if let Some(reply) = reply.borrow_mut().take() { + let _ = reply.send(Ok(json!({"id": selection}))); + } + } + Self::Callback(callback) => { + if let Some(mut callback) = callback.borrow_mut().take() { + callback(selection.map(str::to_string)); + } + } + } + } + + pub fn reject(&self, error: DesktopError) { + match self { + Self::Desktop { reply } => { + if let Some(reply) = reply.borrow_mut().take() { + let _ = reply.send(Err(error)); + } + } + Self::Callback(callback) => { + if let Some(mut callback) = callback.borrow_mut().take() { + callback(None); + } + } + } + } +} + +pub fn target_window(cx: &mut App) -> Result<(AnyWindowHandle, isize), DesktopError> { + let window = cx + .active_window() + .or_else(|| { + cx.global::() + .0 + .get(crate::desktop::windows::MAIN_WINDOW_KEY) + .map(|entry| entry.handle) + }) + .ok_or_else(|| DesktopError::failed("no window for the context menu"))?; + let native_view = native_view_of(cx, window); + Ok((window, native_view)) +} + +pub trait ContextMenuPresenter: 'static { + fn show(&self, cx: &mut App, request: ContextMenuRequest, responder: Responder); + + fn name(&self) -> &'static str; +} + +pub struct ContextMenuPresenterGlobal(pub Rc); +impl gpui::Global for ContextMenuPresenterGlobal {} + +pub fn install(cx: &mut App) { + let fake = cfg!(feature = "test-driver") + && std::env::var("RN_GPUI_TEST_FAKE_NATIVE_UI").as_deref() == Ok("1"); + if fake { + let fake = Rc::new(FakeContextMenu::new()); + cx.set_global(FakeContextMenuGlobal(fake.clone())); + cx.set_global(ContextMenuPresenterGlobal(fake)); + } else if crate::desktop::gpui_menu::should_use_gpui() { + cx.set_global(ContextMenuPresenterGlobal(Rc::new(GpuiContextMenu))); + } else { + cx.set_global(ContextMenuPresenterGlobal(Rc::new(NativeContextMenu))); + } +} + +/// Answers through `reply` when the menu closes. +pub fn show( + cx: &mut App, + items: Value, + x: Option, + y: Option, + window_id: Option, + responder: Responder, +) -> Result<(), DesktopError> { + let model = menu_model::parse_menu(&items)?; + let window = match window_id { + Some(key) => cx + .global::() + .0 + .get(key) + .map(|entry| entry.handle) + .ok_or_else(|| DesktopError::not_found(format!("window {key} is not open")))?, + // A hidden or off-screen window is not the platform's active window, so + // fall back to the main window rather than failing. + None => cx + .active_window() + .or_else(|| { + cx.global::() + .0 + .get(crate::desktop::windows::MAIN_WINDOW_KEY) + .map(|entry| entry.handle) + }) + .ok_or_else(|| DesktopError::failed("no window for the context menu"))?, + }; + let native_view = native_view_of(cx, window); + let (x, y) = match (x, y) { + (Some(x), Some(y)) => (x as f32, y as f32), + _ => cursor_position(cx, window), + }; + let request = ContextMenuRequest { + window, + x, + y, + model, + native_view, + }; + let presenter = cx.global::().0.clone(); + presenter.show(cx, request, responder); + Ok(()) +} + +/// Presents an already-parsed model at a position in a known window. +/// +/// The split exists for the native `ContextMenuView`, which parses its own props +/// before it gets here and knows which window the press arrived in. Sharing the +/// tail means the JS command and the native path cannot disagree about which +/// window hosts the menu or how the platform view is found. +pub fn present( + cx: &mut App, + model: MenuModel, + window: AnyWindowHandle, + position: (f32, f32), + responder: Responder, +) { + let request = ContextMenuRequest { + window, + x: position.0, + y: position.1, + model, + native_view: native_view_of(cx, window), + }; + let presenter = cx.global::().0.clone(); + presenter.show(cx, request, responder); +} + +fn native_view_of(cx: &mut App, window: AnyWindowHandle) -> isize { + window + .update(cx, |_root, window, _cx| raw_view(window)) + .unwrap_or(0) +} + +#[cfg(target_os = "macos")] +fn raw_view(window: &gpui::Window) -> isize { + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + match HasWindowHandle::window_handle(window) { + Ok(handle) => match handle.as_raw() { + RawWindowHandle::AppKit(appkit) => appkit.ns_view.as_ptr() as isize, + _ => 0, + }, + Err(_) => 0, + } +} + +#[cfg(target_os = "windows")] +fn raw_view(window: &gpui::Window) -> isize { + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + match HasWindowHandle::window_handle(window) { + Ok(handle) => match handle.as_raw() { + RawWindowHandle::Win32(win32) => win32.hwnd.get() as isize, + _ => 0, + }, + Err(_) => 0, + } +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +fn raw_view(_window: &gpui::Window) -> isize { + 0 +} + +fn cursor_position(cx: &mut App, window: AnyWindowHandle) -> (f32, f32) { + window + .update(cx, |_root, window, _cx| { + let position = window.mouse_position(); + (f32::from(position.x), f32::from(position.y)) + }) + .unwrap_or((0.0, 0.0)) +} + +struct NativeContextMenu; + +impl ContextMenuPresenter for NativeContextMenu { + fn name(&self) -> &'static str { + "native" + } + + fn show(&self, cx: &mut App, request: ContextMenuRequest, responder: Responder) { + // The native call runs a modal tracking loop, so it is deferred: GPUI + // must not be inside an update while the menu is open. + cx.defer(move |_cx| { + if let Err(error) = native_show(&request, &responder) { + responder.reject(error); + } + }); + } +} + +struct GpuiContextMenu; + +impl ContextMenuPresenter for GpuiContextMenu { + fn name(&self) -> &'static str { + "gpui" + } + + fn show(&self, cx: &mut App, request: ContextMenuRequest, responder: Responder) { + if let Err(error) = crate::desktop::gpui_menu::present(cx, request, responder.clone()) { + responder.reject(DesktopError::failed(error)); + } + } +} + +#[cfg(any(target_os = "macos", target_os = "windows"))] +fn native_show(request: &ContextMenuRequest, responder: &Responder) -> Result<(), DesktopError> { + use muda::ContextMenu as _; + + let menu = muda::Menu::new(); + build_muda_menu(&menu, &request.model.items)?; + let position = muda::dpi::Position::Logical(muda::dpi::LogicalPosition::new( + f64::from(request.x), + f64::from(request.y), + )); + if request.native_view == 0 { + return Err(DesktopError::failed( + "the window has no platform view for a context menu", + )); + } + // `muda` has one event channel and the app installs a handler on it (the + // menu bar and the tray both need one), so a handler-displaced receiver + // would never see this selection. `with_context_menu` reserves the channel + // for the duration of the menu instead, and `show` blocks until the menu + // closes, so the id is already in hand when it returns. + let selection = crate::desktop::menu_events::with_context_menu(|| { + #[cfg(target_os = "macos")] + // SAFETY: `request.native_view` is the live `NSView` the request was made for, and this runs on the main thread. + unsafe { + menu.show_context_menu_for_nsview( + request.native_view as *const std::ffi::c_void, + Some(position), + ) + }; + #[cfg(target_os = "windows")] + // SAFETY: `request.native_view` is the live `HWND` the request was made for, and this runs on the window's thread. + unsafe { + menu.show_context_menu_for_hwnd(request.native_view, Some(position)) + }; + }); + responder.resolve(selection.as_deref()); + Ok(()) +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +fn native_show(_request: &ContextMenuRequest, _responder: &Responder) -> Result<(), DesktopError> { + Err(DesktopError::unsupported( + "native context menus are not implemented on this platform yet", + )) +} + +#[cfg(any(target_os = "macos", target_os = "windows"))] +/// `Menu` and `Submenu` both append items, but the `ContextMenu` trait does not +/// cover it, so builders (context menus and the Windows menu bar) are generic +/// over this. +pub(crate) trait MudaAppend { + fn append_all(&self, items: &[&dyn muda::IsMenuItem]) -> muda::Result<()>; +} + +#[cfg(any(target_os = "macos", target_os = "windows"))] +impl MudaAppend for muda::Menu { + fn append_all(&self, items: &[&dyn muda::IsMenuItem]) -> muda::Result<()> { + self.append_items(items) + } +} + +#[cfg(any(target_os = "macos", target_os = "windows"))] +impl MudaAppend for muda::Submenu { + fn append_all(&self, items: &[&dyn muda::IsMenuItem]) -> muda::Result<()> { + self.append_items(items) + } +} + +#[cfg(any(target_os = "macos", target_os = "windows"))] +fn build_muda_menu(menu: &impl MudaAppend, items: &[MenuNode]) -> Result<(), DesktopError> { + use muda::{CheckMenuItem, IconMenuItem, MenuItem, PredefinedMenuItem, Submenu}; + + let mut children: Vec> = Vec::new(); + for node in items { + match node { + MenuNode::Separator => { + children.push(Box::new(PredefinedMenuItem::separator())); + } + MenuNode::StandardMenu(_) => {} + MenuNode::Item(item) => { + if item.checked { + children.push(Box::new(CheckMenuItem::with_id( + item.id.clone(), + item.label.clone(), + item.enabled, + true, + None, + ))); + } else if let Some(icon) = item.icon.as_deref().and_then(muda_icon) { + children.push(Box::new(IconMenuItem::with_id( + item.id.clone(), + item.label.clone(), + item.enabled, + Some(icon), + None, + ))); + } else { + children.push(Box::new(MenuItem::with_id( + item.id.clone(), + item.label.clone(), + item.enabled, + None, + ))); + } + } + MenuNode::Submenu(submenu) => { + let child = Submenu::new(submenu.label.clone(), submenu.enabled); + build_muda_menu(&child, &submenu.items)?; + children.push(Box::new(child)); + } + } + } + let refs: Vec<&dyn muda::IsMenuItem> = children.iter().map(|item| item.as_ref()).collect(); + menu.append_all(&refs) + .map_err(|error| DesktopError::failed(format!("could not build the context menu: {error}"))) +} + +/// A menu item's icon, if `desktop::icon` has loaded it. +#[cfg(any(target_os = "macos", target_os = "windows"))] +pub(crate) fn muda_icon(uri: &str) -> Option { + let icon = crate::desktop::icon::cached(uri)?; + muda::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height).ok() +} + +pub struct FakeContextMenu { + state: RefCell, +} + +#[derive(Default)] +struct FakeContextMenuState { + model: Option, + responder: Option, + position: (f32, f32), + shown: u32, +} + +pub struct FakeContextMenuGlobal(pub Rc); +impl gpui::Global for FakeContextMenuGlobal {} + +impl FakeContextMenu { + pub fn new() -> Self { + Self { + state: RefCell::new(FakeContextMenuState::default()), + } + } +} + +impl ContextMenuPresenter for FakeContextMenu { + fn name(&self) -> &'static str { + "fake" + } + + fn show(&self, _cx: &mut App, request: ContextMenuRequest, responder: Responder) { + let mut state = self.state.borrow_mut(); + // A second show dismisses the first with `null`. + if let Some(previous) = state.responder.take() { + previous.resolve(None); + } + state.model = Some(json!({"items": nodes_json(&request.model.items)})); + state.position = (request.x, request.y); + state.shown += 1; + state.responder = Some(responder); + } +} + +pub fn nodes_json(items: &[MenuNode]) -> Vec { + items + .iter() + .map(|node| match node { + MenuNode::Separator => json!({"type": "separator"}), + MenuNode::StandardMenu(role) => json!({"type": "role", "role": format!("{role:?}")}), + MenuNode::Submenu(submenu) => json!({ + "type": "submenu", + "id": submenu.id, + "label": submenu.label, + "enabled": submenu.enabled, + "items": nodes_json(&submenu.items), + "icon": submenu.icon, + }), + MenuNode::Item(item) => json!({ + "type": "item", + "id": item.id, + "label": item.label, + "enabled": item.enabled, + "checked": item.checked, + "icon": item.icon, + "iconLoaded": item + .icon + .as_deref() + .is_some_and(|uri| crate::desktop::icon::cached(uri).is_some()), + }), + }) + .collect() +} + +pub fn presenter_name(cx: &App) -> &'static str { + cx.try_global::() + .map(|presenter| presenter.0.name()) + .unwrap_or("none") +} + +pub fn fake_model(cx: &App) -> Option { + let fake = cx.try_global::()?; + let state = fake.0.state.borrow(); + Some(json!({ + "items": state.model.as_ref().and_then(|value| value.get("items")).cloned().unwrap_or(Value::Null), + "x": state.position.0, + "y": state.position.1, + "shown": state.shown, + })) +} + +pub fn fake_select(cx: &App, id: &str) -> bool { + let Some(fake) = cx.try_global::() else { + return false; + }; + let responder = fake.0.state.borrow_mut().responder.take(); + match responder { + Some(responder) => { + responder.resolve(Some(id)); + true + } + None => false, + } +} + +pub fn fake_dismiss(cx: &App) -> bool { + let Some(fake) = cx.try_global::() else { + return false; + }; + let responder = fake.0.state.borrow_mut().responder.take(); + match responder { + Some(responder) => { + responder.resolve(None); + true + } + None => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn rejects_a_malformed_model_before_presenting() { + let error = menu_model::parse_menu(&json!([{"id": "a"}])).expect_err("label is required"); + assert_eq!(error.code, crate::desktop::errors::codes::INVALID_ARGUMENT); + } + + #[test] + fn nodes_json_keeps_ids_and_checks() { + let model = menu_model::parse_menu(&json!([ + {"id": "open", "label": "Open"}, + {"type": "separator"}, + {"id": "pin", "label": "Pin", "checked": true} + ])) + .expect("valid"); + let value = nodes_json(&model.items); + assert_eq!(value.len(), 3); + assert_eq!(value[2]["checked"], json!(true)); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/dev_menu.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/dev_menu.rs new file mode 100644 index 0000000000..9c253c47cc --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/dev_menu.rs @@ -0,0 +1,610 @@ +use crate::desktop::menu_model::{MenuItemDef, MenuModel, MenuNode}; +use crate::desktop::perf_monitor; +use crate::desktop::windows::{DesktopWindowsGlobal, MAIN_WINDOW_KEY}; +use crate::store::store_from; +use gpui::{App, Global}; +use serde_json::{json, Value}; +use std::cell::RefCell; + +pub const RELOAD: &str = "dev:reload"; +pub const OPEN_DEVTOOLS: &str = "dev:openDevTools"; +pub const ELEMENT_INSPECTOR: &str = "dev:elementInspector"; +pub const PERF_MONITOR: &str = "dev:perfMonitor"; +pub const FAST_REFRESH: &str = "dev:fastRefresh"; +pub const ACCELERATOR: &str = "secondary-d"; +pub const ACCELERATOR_ID: &str = "dev:menu"; + +#[derive(Default)] +pub struct DevSettings { + pub installed: bool, + pub hot_loading: bool, + pub profiling: bool, + pub inspector: bool, +} + +pub struct DevSettingsGlobal(pub RefCell); +impl Global for DevSettingsGlobal {} + +pub fn is_installed(cx: &App) -> bool { + cx.try_global::() + .is_some_and(|state| state.0.borrow().installed) +} + +pub fn install(cx: &mut App, enabled: bool) { + cx.set_global(DevSettingsGlobal(RefCell::new(DevSettings { + installed: enabled, + hot_loading: true, + ..Default::default() + }))); +} + +pub fn menu_model(cx: &App) -> MenuModel { + let (hot_loading, inspector) = cx + .try_global::() + .map(|state| { + let state = state.0.borrow(); + (state.hot_loading, state.inspector) + }) + .unwrap_or((false, false)); + let profiling = perf_monitor::is_enabled(cx); + let item = |id: &str, label: &str, checked: bool| { + MenuNode::Item(MenuItemDef { + id: id.to_string(), + label: label.to_string(), + accelerator: None, + enabled: true, + checked, + role: None, + icon: None, + }) + }; + MenuModel { + items: vec![ + item(RELOAD, "Reload", false), + item(OPEN_DEVTOOLS, "Open DevTools", false), + MenuNode::Separator, + item(ELEMENT_INSPECTOR, "Toggle Element Inspector", inspector), + item(PERF_MONITOR, "Toggle Perf Monitor", profiling), + item(FAST_REFRESH, "Fast Refresh", hot_loading), + ], + } +} + +pub fn show(cx: &mut App) { + if !is_installed(cx) { + log::warn!("[desktop][devmenu] the Dev Menu is not enabled in this build"); + return; + } + // The panel may have been closed since it was recorded, so liveness is + // asked of the window rather than remembered. + if let Some(handle) = open_panel(cx) { + let _ = handle.update(cx, |_root: gpui::AnyView, window, _cx| { + window.activate_window(); + }); + return; + } + // `WindowKind::Dialog` is a sheet on macOS, so this is a normal window + // centred over the app window. + let size = gpui::size(gpui::px(380.0), gpui::px(460.0)); + let bounds = main_window(cx) + .and_then(|handle| { + handle + .update(cx, |_root: gpui::AnyView, window, _cx| window.bounds()) + .ok() + }) + .map(|parent| { + gpui::Bounds::new( + gpui::point( + parent.origin.x + (parent.size.width - size.width) / 2.0, + parent.origin.y + (parent.size.height - size.height) / 2.0, + ), + size, + ) + }) + .unwrap_or_else(|| gpui::Bounds::centered(None, size, cx)); + let options = gpui::WindowOptions { + window_bounds: Some(gpui::WindowBounds::Windowed(bounds)), + kind: gpui::WindowKind::Normal, + is_resizable: false, + window_min_size: Some(size), + titlebar: Some(gpui::TitlebarOptions { + title: Some("Dev Menu".into()), + appears_transparent: false, + traffic_light_position: None, + }), + ..Default::default() + }; + match cx.open_window(options, |_window, cx| { + gpui::AppContext::new(cx, |_cx| DevMenuPanel::new()) + }) { + Ok(handle) => { + let handle: gpui::AnyWindowHandle = handle.into(); + cx.set_global(DevMenuPanelGlobal(RefCell::new(Some(handle)))); + let _ = handle.update(cx, |_root: gpui::AnyView, window, _cx| { + crate::desktop::platform_window::disable_window_animation(window); + }); + log::info!("[desktop][devmenu] opened the panel"); + } + Err(error) => log::error!("[desktop][devmenu] could not open the panel: {error}"), + } +} + +pub fn hide(cx: &mut App) { + let Some(handle) = open_panel(cx) else { + forget_panel(cx); + return; + }; + forget_panel(cx); + let _ = handle.update(cx, |_root: gpui::AnyView, window, _cx| { + window.remove_window() + }); +} + +pub struct DevMenuPanelGlobal(pub RefCell>); +impl Global for DevMenuPanelGlobal {} + +fn open_panel(cx: &mut App) -> Option { + let handle = cx + .try_global::() + .and_then(|panel| *panel.0.borrow())?; + handle + .update(cx, |_root: gpui::AnyView, _window, _cx| ()) + .ok()?; + Some(handle) +} + +fn forget_panel(cx: &mut App) { + if let Some(panel) = cx.try_global::() { + panel.0.borrow_mut().take(); + } +} + +const DEFAULT_BUNDLER_PORT: u32 = 8081; + +fn parse_bundler(text: &str) -> Result<(String, u32), &'static str> { + let text = text.trim(); + let text = text + .strip_prefix("http://") + .or_else(|| text.strip_prefix("https://")) + .unwrap_or(text) + .trim_end_matches('/'); + let (host, port) = match text.rsplit_once(':') { + Some((host, port)) => ( + host, + port.parse::() + .ok() + .filter(|port| (1..=65535).contains(port)) + .ok_or("The port must be a number from 1 to 65535.")?, + ), + None => (text, DEFAULT_BUNDLER_PORT), + }; + if host.is_empty() { + return Err("Enter the bundler's address as host:port."); + } + Ok((host.to_string(), port)) +} + +fn is_bundler_char(c: char) -> bool { + c.is_ascii_alphanumeric() || matches!(c, '.' | ':' | '-' | '_' | '/' | '[' | ']') +} + +pub struct DevMenuPanel { + bundler: Option, + bundler_error: Option<&'static str>, + focus: Option, +} + +impl DevMenuPanel { + pub fn new() -> Self { + Self { + bundler: None, + bundler_error: None, + focus: None, + } + } + + fn render_bundler( + &mut self, + text: String, + window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> gpui::Stateful { + use gpui::{InteractiveElement, ParentElement, Styled}; + + let focus = self.focus.get_or_insert_with(|| cx.focus_handle()).clone(); + if !focus.is_focused(window) { + window.focus(&focus, cx); + } + gpui::div() + .id(gpui::SharedString::from("dev-menu-bundler")) + .track_focus(&focus) + .size_full() + .flex() + .flex_col() + .gap_2() + .p_3() + .bg(gpui::rgb(0xf4f5f7)) + .text_color(gpui::rgb(0x18212f)) + .on_key_down(cx.listener(|this, event: &gpui::KeyDownEvent, window, cx| { + let Some(text) = this.bundler.as_mut() else { + return; + }; + let keystroke = &event.keystroke; + let command = keystroke.modifiers.platform || keystroke.modifiers.control; + match keystroke.key.as_str() { + "escape" => { + this.bundler = None; + this.bundler_error = None; + } + "backspace" => { + text.pop(); + this.bundler_error = None; + } + "enter" => match parse_bundler(text) { + Ok((host, port)) => { + let applied = store_from(cx).is_some_and(|store| { + store.read(cx).host.set_dev_server(&host, port) + }); + if applied { + log::info!( + "[desktop][devmenu] bundler set to {host}:{port}; reloading" + ); + forget_panel(cx); + window.remove_window(); + return; + } + this.bundler_error = + Some("This build is not connected to a dev server."); + } + Err(error) => this.bundler_error = Some(error), + }, + "v" if command => { + if let Some(pasted) = cx.read_from_clipboard().and_then(|item| item.text()) + { + text.extend(pasted.chars().filter(|c| is_bundler_char(*c))); + this.bundler_error = None; + } + } + _ if command => {} + _ => { + if let Some(typed) = keystroke.key_char.as_ref() { + text.extend(typed.chars().filter(|c| is_bundler_char(*c))); + this.bundler_error = None; + } + } + } + cx.stop_propagation(); + cx.notify(); + })) + .child( + gpui::div() + .px_3() + .py_2() + .text_size(gpui::px(18.0)) + .child("Configure Bundler"), + ) + .child( + gpui::div() + .px_3() + .text_size(gpui::px(13.0)) + .text_color(gpui::rgba(0x000000aa)) + .child("The Metro server to load the bundle from, as host:port."), + ) + .child( + gpui::div() + .mx_3() + .px_2() + .py_2() + .rounded(gpui::px(3.0)) + .border_1() + .border_color(gpui::rgb(0x3b82f6)) + .bg(gpui::white()) + .text_size(gpui::px(14.0)) + // The caret: the field is always at the end of the text. + .child(format!("{text}\u{2502}")), + ) + .children(self.bundler_error.map(|error| { + gpui::div() + .px_3() + .text_size(gpui::px(13.0)) + .text_color(gpui::rgb(0xb91c1c)) + .child(error) + })) + .child( + gpui::div() + .px_3() + .text_size(gpui::px(13.0)) + .text_color(gpui::rgba(0x00000099)) + .child("Enter applies it and reloads. Escape goes back."), + ) + } +} + +impl gpui::Render for DevMenuPanel { + fn render( + &mut self, + window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> impl gpui::IntoElement { + use gpui::{InteractiveElement, IntoElement, ParentElement, Styled}; + + window.on_window_should_close(cx, |_window, cx| { + forget_panel(cx); + true + }); + + if let Some(text) = self.bundler.clone() { + return self.render_bundler(text, window, cx); + } + let bundler = store_from(cx) + .map(|store| store.read(cx).host.dev_server_address()) + .unwrap_or_default(); + + let rows = menu_model(cx); + let mut elements: Vec = Vec::new(); + for node in rows.items { + match node { + MenuNode::Separator => { + elements.push( + gpui::div() + .h(gpui::px(1.0)) + .my_1() + .bg(gpui::rgba(0x00000022)) + .into_any_element() as gpui::AnyElement, + ); + } + MenuNode::Item(item) => { + let id = item.id.clone(); + elements.push( + gpui::div() + .id(gpui::SharedString::from(format!("dev-menu-{}", item.id))) + .flex() + .flex_row() + .items_center() + .justify_between() + .px_3() + .py_2() + .rounded(gpui::px(3.0)) + .text_size(gpui::px(14.0)) + .cursor_pointer() + .hover(|style| style.bg(gpui::rgba(0x00000011))) + .child(item.label.clone()) + .child(if item.checked { "✓" } else { "" }) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(move |_this, _event, window, cx| { + if let Some(handler) = + rn_gpui_sys::DEV_MENU_PERFORM_HANDLER.get() + { + handler(id.clone()); + } + forget_panel(cx); + window.remove_window(); + }), + ) + .into_any_element(), + ); + } + MenuNode::Submenu(_) | MenuNode::StandardMenu(_) => {} + } + } + + gpui::div() + .id(gpui::SharedString::from("dev-menu-panel")) + .size_full() + .flex() + .flex_col() + .gap_1() + .p_3() + .bg(gpui::rgb(0xf4f5f7)) + .text_color(gpui::rgb(0x18212f)) + .child( + gpui::div() + .px_3() + .py_2() + .text_size(gpui::px(18.0)) + .child("Dev Menu"), + ) + .children(elements) + // Only with a dev server: a release bundle has nowhere else to + // load from. + .children((!bundler.is_empty()).then(|| { + gpui::div() + .id(gpui::SharedString::from("dev-menu-bundler-row")) + .flex() + .flex_row() + .items_center() + .justify_between() + .px_3() + .py_2() + .rounded(gpui::px(3.0)) + .text_size(gpui::px(14.0)) + .cursor_pointer() + .hover(|style| style.bg(gpui::rgba(0x00000011))) + .child("Configure Bundler…") + .child( + gpui::div() + .text_color(gpui::rgba(0x00000099)) + .child(bundler.clone()), + ) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(move |this, _event, _window, cx| { + this.bundler = Some(bundler.clone()); + this.bundler_error = None; + cx.notify(); + }), + ) + })) + .child( + gpui::div() + .id(gpui::SharedString::from("dev-menu-close")) + .mt_2() + .px_3() + .py_2() + .rounded(gpui::px(3.0)) + .text_size(gpui::px(14.0)) + .text_color(gpui::rgba(0x00000099)) + .cursor_pointer() + .hover(|style| style.bg(gpui::rgba(0x00000011))) + .child("Close") + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(|_this, _event, window, cx| { + forget_panel(cx); + window.remove_window(); + }), + ), + ) + } +} + +pub fn is_command(id: &str) -> bool { + matches!( + id, + RELOAD | OPEN_DEVTOOLS | ELEMENT_INSPECTOR | PERF_MONITOR | FAST_REFRESH + ) +} + +pub fn perform(cx: &mut App, id: &str) { + match id { + RELOAD => { + if let Some(store) = store_from(cx) { + store.read(cx).host.request_reload(); + } + } + OPEN_DEVTOOLS => { + if let Some(store) = store_from(cx) { + store.read(cx).host.open_debugger(); + } + emit(cx, "gpuiDesktop:devToolsRequested", json!({})); + } + ELEMENT_INSPECTOR => { + let enabled = toggle(cx, "inspector"); + // The event React Native's `AppContainer` listens for; every + // platform's dev menu toggles the inspector by emitting it. + emit(cx, "toggleElementInspector", json!({})); + emit( + cx, + "gpuiDesktop:elementInspector", + json!({"enabled": enabled}), + ); + } + PERF_MONITOR => { + let enabled = !perf_monitor::is_enabled(cx); + perf_monitor::set_enabled(cx, enabled); + } + FAST_REFRESH => { + let enabled = toggle(cx, "hotLoading"); + emit(cx, "gpuiDesktop:fastRefresh", json!({"enabled": enabled})); + } + _ => {} + } +} + +pub fn apply_setting(cx: &mut App, name: &str, enabled: bool) { + match name { + "profiling" => { + perf_monitor::set_enabled(cx, enabled); + } + "hotLoading" => { + if let Some(state) = cx.try_global::() { + state.0.borrow_mut().hot_loading = enabled; + } + emit(cx, "gpuiDesktop:fastRefresh", json!({"enabled": enabled})); + } + // `DevSettings.toggleElementInspector` has no value to pass. + "inspectorToggle" => { + let enabled = toggle(cx, "inspector"); + emit(cx, "toggleElementInspector", json!({})); + emit( + cx, + "gpuiDesktop:elementInspector", + json!({"enabled": enabled}), + ); + } + "inspector" => { + if let Some(state) = cx.try_global::() { + state.0.borrow_mut().inspector = enabled; + } + emit( + cx, + "gpuiDesktop:elementInspector", + json!({"enabled": enabled}), + ); + } + _ => {} + } +} + +fn toggle(cx: &App, which: &str) -> bool { + let Some(state) = cx.try_global::() else { + return false; + }; + let mut state = state.0.borrow_mut(); + let value = match which { + "inspector" => &mut state.inspector, + "hotLoading" => &mut state.hot_loading, + "profiling" => &mut state.profiling, + _ => return false, + }; + *value = !*value; + *value +} + +fn emit(cx: &App, name: &str, payload: Value) { + let Some(store) = store_from(cx) else { + return; + }; + store + .read(cx) + .host + .emit_device_event_json(name, &payload.to_string()); +} + +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] +pub fn main_window(cx: &App) -> Option { + cx.try_global::() + .and_then(|windows| windows.0.get(MAIN_WINDOW_KEY).map(|entry| entry.handle)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accelerator_is_command_or_control_d() { + assert_eq!(ACCELERATOR, "secondary-d"); + assert_eq!(ACCELERATOR_ID, "dev:menu"); + } + + #[test] + fn bundler_addresses_parse_with_and_without_a_port() { + assert_eq!( + parse_bundler("localhost:8082"), + Ok(("localhost".into(), 8082)) + ); + assert_eq!(parse_bundler(" 10.0.0.5 "), Ok(("10.0.0.5".into(), 8081))); + assert_eq!( + parse_bundler("http://my-mac.local:9000/"), + Ok(("my-mac.local".into(), 9000)) + ); + assert!(parse_bundler("").is_err()); + assert!(parse_bundler(":8081").is_err()); + assert!(parse_bundler("localhost:0").is_err()); + assert!(parse_bundler("localhost:port").is_err()); + } + + #[test] + fn dev_item_ids_are_unique() { + let ids = [ + RELOAD, + OPEN_DEVTOOLS, + ELEMENT_INSPECTOR, + PERF_MONITOR, + FAST_REFRESH, + ]; + let unique: std::collections::HashSet<_> = ids.iter().collect(); + assert_eq!(unique.len(), ids.len()); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/errors.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/errors.rs new file mode 100644 index 0000000000..6ba05d82bd --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/errors.rs @@ -0,0 +1,59 @@ +//! A rejection is the JSON encoding of this struct: the C++ promise only +//! carries a string. + +#[derive(Clone, Debug)] +pub struct DesktopError { + pub code: &'static str, + pub message: String, +} + +pub mod codes { + pub const UNSUPPORTED: &str = "unsupported"; + pub const INVALID_ARGUMENT: &str = "invalid_argument"; + pub const NOT_FOUND: &str = "not_found"; + pub const RELOADED: &str = "reloaded"; + pub const FAILED: &str = "failed"; +} + +impl DesktopError { + pub fn new(code: &'static str, message: impl Into) -> Self { + Self { + code, + message: message.into(), + } + } + + pub fn unsupported(message: impl Into) -> Self { + Self::new(codes::UNSUPPORTED, message) + } + + pub fn invalid_argument(message: impl Into) -> Self { + Self::new(codes::INVALID_ARGUMENT, message) + } + + pub fn not_found(message: impl Into) -> Self { + Self::new(codes::NOT_FOUND, message) + } + + pub fn failed(message: impl Into) -> Self { + Self::new(codes::FAILED, message) + } + + pub fn to_json(&self) -> String { + serde_json::json!({"code": self.code, "message": self.message}).to_string() + } + + pub fn to_json_value(&self) -> serde_json::Value { + serde_json::json!({"error": {"code": self.code, "message": self.message}}) + } +} + +impl std::fmt::Display for DesktopError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.code, self.message) + } +} + +impl std::error::Error for DesktopError {} + +pub type DesktopResult = Result; diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/gpui_menu.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/gpui_menu.rs new file mode 100644 index 0000000000..cec8027a49 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/gpui_menu.rs @@ -0,0 +1,969 @@ +//! Linux has no native menu GPUI can use (`muda` needs a GTK window), so GPUI +//! draws it. `RN_GPUI_CONTEXT_MENU=gpui` / `RN_GPUI_MENU_BAR=gpui` select it +//! on other platforms, for tests. + +use crate::desktop::context_menu::{ContextMenuRequest, Responder}; +use crate::desktop::menu_model::{MenuModel, MenuNode}; +use gpui::prelude::FluentBuilder as _; +use gpui::{ + point, px, size, App, AppContext, Bounds, Context, InteractiveElement, IntoElement, + ParentElement, Render, SharedString, Styled, Window, WindowBounds, WindowKind, WindowOptions, +}; +use std::cell::RefCell; + +pub const MENU_BAR_HEIGHT: f32 = 26.0; + +const ROW_HEIGHT: f32 = 26.0; +const PADDING: f32 = 6.0; +const MIN_WIDTH: f32 = 160.0; +const MAX_WIDTH: f32 = 420.0; + +/// A GPUI popup's elements are not RN nodes, so the test driver drives the +/// open popup through this. +pub struct PendingMenu { + pub window: gpui::AnyWindowHandle, + pub items: Vec, + pub responder: Responder, +} + +pub struct PendingMenuGlobal(pub RefCell>); +impl gpui::Global for PendingMenuGlobal {} + +/// Closes the menu currently tracked in `PendingMenuGlobal`, if any. +/// +/// Only one top-level GPUI menu is meant to be up at a time. Opening a second +/// one used to overwrite the global and orphan the first window: nothing else +/// reaps a popup, so it stayed on screen (and on top) until the process exited, +/// and a repeated right-click left a stack of them. Every opener runs this +/// first, so a new menu replaces the old one instead of joining it. +fn close_open_menu(cx: &mut App) { + let Some(pending) = cx.try_global::() else { + return; + }; + let taken = pending.0.borrow_mut().take(); + if let Some(menu) = taken { + let _ = menu + .window + .update(cx, |_root, window, _cx| window.remove_window()); + } +} + +pub fn should_use_gpui() -> bool { + match std::env::var("RN_GPUI_CONTEXT_MENU").ok().as_deref() { + Some("gpui") => true, + Some("native") => false, + _ => cfg!(target_os = "linux"), + } +} + +pub fn should_use_gpui_menu_bar() -> bool { + match std::env::var("RN_GPUI_MENU_BAR").ok().as_deref() { + Some("gpui") => true, + Some("native") => false, + _ => cfg!(target_os = "linux"), + } +} + +/// The inset the React surface leaves at the top of a window for the menu bar. +/// +/// This is the process-wide answer, for callers with no window in hand. A caller +/// that knows its window must use [`menu_bar_inset_for`], because the bar can be +/// hidden for one window and shown in another — the main window and a popover can +/// disagree, and the surface size has to follow the window it belongs to. +pub fn menu_bar_inset() -> f32 { + if should_use_gpui_menu_bar() { + MENU_BAR_HEIGHT + } else { + 0.0 + } +} + +/// The inset for one window, honouring a runtime hide. +pub fn menu_bar_inset_for(_cx: &gpui::App, window_id: u32) -> f32 { + menu_bar_inset_for_window_id(window_id) +} + +/// The inset for a window id, for callers with no `Context`. +/// +/// `ViewStore::resize_window` is one: it is handed an `AnyWindowHandle` and the +/// surface id, and the thread local holds the answer without needing `cx`. +pub fn menu_bar_inset_for_window_id(window_id: u32) -> f32 { + if !should_use_gpui_menu_bar() || menu_bar_hidden(window_id) { + 0.0 + } else { + MENU_BAR_HEIGHT + } +} + +/// Whether this window draws its menu bar. +/// +/// macOS answers `false` always: its menu bar is the system's, and there is +/// nothing in the window to hide. The capability `menuBarInWindow` reports the +/// same thing to JS, so an app can hide its own UI on the one platform where +/// hiding does nothing. +pub fn menu_bar_in_window(window_id: u32) -> bool { + should_use_gpui_menu_bar() && !menu_bar_hidden(window_id) +} + +pub fn menu_bar_hidden(window_id: u32) -> bool { + HIDDEN_MENU_BARS.with(|hidden| hidden.borrow().contains(&window_id)) +} + +thread_local! { + /// Windows whose bar was hidden, mirrored here so the answer is available + /// without a `Context` — `menu_bar_inset` is called from window plumbing that + /// has one, but the JS getter and the tests do not. + static HIDDEN_MENU_BARS: std::cell::RefCell> = + std::cell::RefCell::new(std::collections::HashSet::new()); +} + +/// Re-applies the surface size after the inset changed. +/// +/// The React surface is laid out to the window's size minus the inset, and the +/// window will not send a resize of its own just because the bar went away, so +/// the store has to be told the new answer. +pub fn resize_surfaces_for_menu_bar(cx: &mut gpui::App, window_id: u32) { + let Some(store) = cx.try_global::() else { + return; + }; + let store = store.0.clone(); + let handle = { + let windows = cx.global::(); + windows.0.get(window_id).map(|entry| entry.handle) + }; + let Some(handle) = handle else { + return; + }; + let size = handle + .update(cx, |_, window, _| window.viewport_size()) + .ok(); + let Some(size) = size else { + return; + }; + let scale = handle + .update(cx, |_, window, _| window.scale_factor()) + .unwrap_or(1.0); + store.update(cx, |store, _| { + store.resize_window(handle, f32::from(size.width), f32::from(size.height), scale); + }); +} + +/// Shows or hides the menu bar of one window. +/// +/// The set lives in a thread local rather than in `AppMenuGlobal` because a global +/// is handed out by shared reference and the attached `muda::Menu` on Windows +/// makes the state impossible to clone and replace. One set, one source of truth: +/// the inset and the JS getter both read it. +pub fn set_menu_bar_visible(window_id: u32, visible: bool) { + HIDDEN_MENU_BARS.with(|hidden| { + let mut hidden = hidden.borrow_mut(); + if visible { + hidden.remove(&window_id); + } else { + hidden.insert(window_id); + } + }); +} + +pub struct MenuBar { + #[allow(dead_code)] + window_id: u32, +} + +impl MenuBar { + pub fn new(window_id: u32) -> Self { + Self { window_id } + } +} + +/// Creates the menu bar and its activation observer, without touching the root. +/// +/// Called from inside a `window.update` whose closure has already leased the +/// surface root, so this must not update the root: that panics with "cannot +/// update SurfaceRootView while it is already being updated". The observer is +/// registered on the menu bar entity (a fresh entity, safe to update here) and +/// its callback only dismisses the popup. +/// +/// The observer closes a menu left up when the user moves on: clicking anywhere +/// outside the app, another window taking focus, or the app being backgrounded. +/// The popup is an override-redirect window on X11, so it never receives that +/// click itself and there is no global grab to catch it; window activation is +/// the signal that is actually delivered (an X11 `FocusOut` calls +/// `set_active(false)`, which runs these observers). +pub fn prepare(window: &mut gpui::Window, cx: &mut App, window_id: u32) -> Option { + if !should_use_gpui_menu_bar() { + return None; + } + let menu_bar = cx.new(|_| MenuBar::new(window_id)); + let subscription = menu_bar.update(cx, |_menu_bar, cx| { + cx.observe_window_activation(window, |_menu_bar, window, cx| { + if window.is_window_active() { + return; + } + // On Windows the popup (and any submenu) is its own top-level + // window, so opening it can deactivate this one. Only dismiss once + // activation has left the app entirely. The popup's activation can + // arrive after this window's deactivation, so check a moment later. + cx.spawn(async move |_menu_bar, cx| { + cx.background_executor() + .timer(std::time::Duration::from_millis(100)) + .await; + let _ = cx.update(|cx| { + if cx.active_window().is_none() { + let _ = MenuPopup::dismiss(cx); + } + }); + }) + .detach(); + }) + }); + Some(PreparedMenuBar { + menu_bar, + subscription, + }) +} + +/// The menu bar `prepare` built, handed to `attach` once the root is free. +pub struct PreparedMenuBar { + pub menu_bar: gpui::Entity, + pub subscription: gpui::Subscription, +} + +pub fn attach( + cx: &mut App, + root: &gpui::Entity, + prepared: PreparedMenuBar, +) { + root.update(cx, |root, cx| { + root.menu_bar = Some(prepared.menu_bar); + root.menu_bar_activation_subscription = Some(prepared.subscription); + cx.notify(); + }); +} + +pub fn menu_bar_entries(cx: &App) -> Vec<(String, Vec)> { + use crate::desktop::app_menu; + use crate::desktop::menu_model::MenuRole; + + let model = app_menu::current_model(cx); + let app_name = app_menu::app_name(cx); + let dev_menu = crate::desktop::dev_menu::is_installed(cx); + let mut titles: Vec<(String, Vec)> = Vec::new(); + for node in &model.items { + match node { + MenuNode::StandardMenu(role) => { + let label = match role { + MenuRole::AppMenu => app_name.clone(), + other => other.default_label().to_string(), + }; + titles.push(( + label, + app_menu::standard_menu_nodes(*role, &app_name, dev_menu), + )); + } + MenuNode::Submenu(submenu) => { + titles.push((submenu.label.clone(), submenu.items.clone())); + } + _ => {} + } + } + titles +} + +fn menu_bar_responder() -> Responder { + Responder::callback(Box::new(|selection| { + if let Some(id) = selection { + if let Some(handler) = rn_gpui_sys::MENU_BAR_PERFORM_HANDLER.get() { + handler(id); + } + } + })) +} + +/// Opens a top-level menu and makes it the one `PendingMenuGlobal` tracks. +/// +/// The single place a root menu is opened: it first closes whatever menu is +/// already up, so a second open replaces the first instead of leaving its window +/// on screen, then records the new window so it can be closed or dismissed +/// later. A submenu (`depth > 0`) is opened with `open_menu_window` directly; it +/// belongs to the menu already up and must not close it. +fn open_tracked_menu( + cx: &mut App, + parent: gpui::AnyWindowHandle, + anchor: (f32, f32), + items: Vec, + responder: Responder, +) -> Result { + close_open_menu(cx); + let window = open_menu_window(cx, parent, anchor, items.clone(), responder.clone(), 0)?; + cx.set_global(PendingMenuGlobal(RefCell::new(Some(PendingMenu { + window, + items, + responder, + })))); + Ok(window) +} + +pub fn open_menu_bar_menu(cx: &mut App, title: &str) -> bool { + let entries = menu_bar_entries(cx); + let Some((_, items)) = entries.into_iter().find(|(label, _)| label == title) else { + return false; + }; + let (window, _) = match crate::desktop::context_menu::target_window(cx) { + Ok(target) => target, + Err(error) => { + log::error!("[gpui-menu] {error}"); + return false; + } + }; + match open_tracked_menu( + cx, + window, + (0.0, MENU_BAR_HEIGHT), + items, + menu_bar_responder(), + ) { + Ok(_) => true, + Err(error) => { + log::error!("[gpui-menu] {error}"); + false + } + } +} + +impl Render for MenuBar { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let titles = menu_bar_entries(cx); + let mut rows: Vec = Vec::new(); + for (label, items) in titles { + let id = label.clone(); + rows.push( + gpui::div() + .id(SharedString::from(format!("menu-bar-{id}"))) + .px_2() + .h(px(MENU_BAR_HEIGHT)) + .flex() + .items_center() + .text_size(px(13.0)) + .cursor_pointer() + .hover(|style| style.bg(gpui::rgba(0x00000014))) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(move |this, event: &gpui::MouseDownEvent, window, cx| { + let parent: gpui::AnyWindowHandle = window.window_handle(); + let responder = menu_bar_responder(); + let anchor = (f32::from(event.position.x), f32::from(event.position.y)); + let menu_items = items.clone(); + let _ = this; + // Deferred, not opened here: this handler runs inside the + // menu-bar window's own update, and the popup's fallback + // reads the parent's bounds with `parent.update(...)`. A + // re-entrant update of the window already on the stack is + // refused and reported as `WindowRemoved` ("window not + // found"), so the menu never opened. Deferring runs after + // the effect cycle, when the window is off the stack. + cx.defer(move |cx| { + if let Err(error) = + open_tracked_menu(cx, parent, anchor, menu_items, responder) + { + log::error!("[gpui-menu] {error}"); + } + }); + }), + ) + .child(label) + .into_any_element(), + ); + } + + gpui::div() + .id(SharedString::from("gpui-menu-bar")) + .w_full() + .h(px(MENU_BAR_HEIGHT)) + .flex() + .flex_row() + .items_center() + .gap_0p5() + .px_1() + .bg(gpui::rgba(0xf4f5f7ff)) + .border_b_1() + .border_color(gpui::rgba(0x00000022)) + .text_color(gpui::rgb(0x18212f)) + .child( + gpui::div() + .text_size(px(11.0)) + .text_color(gpui::rgba(0x00000055)) + .px_1() + .child("‹window›"), + ) + .children(rows) + } +} + +pub fn present( + cx: &mut App, + request: ContextMenuRequest, + responder: Responder, +) -> Result<(), String> { + open_tracked_menu( + cx, + request.window, + (request.x, request.y), + request.model.items, + responder, + ) + .map(|_| ()) +} + +/// Open one dropdown. +/// +/// `AnchoredPopup` is the right window kind — the platform places it next to the +/// anchor and keeps it on screen — but macOS rejects it ("native popups are not +/// implemented on macOS yet"), so the fallback is a floating window placed at +/// the anchor's screen position. Linux (X11 and Wayland) and Windows take the +/// anchored path, which is what the plan asks for there. +fn open_menu_window( + cx: &mut App, + parent: gpui::AnyWindowHandle, + anchor: (f32, f32), + items: Vec, + responder: Responder, + depth: usize, +) -> Result { + let (width, height) = popup_size(&MenuModel { + items: items.clone(), + }); + let popup_options = gpui::popup::PopupOptions { + parent, + anchor_rect: Bounds::new(point(px(anchor.0), px(anchor.1)), size(px(1.0), px(1.0))), + anchor: if depth == 0 { + gpui::popup::PopupAnchor::BottomLeft + } else { + gpui::popup::PopupAnchor::TopRight + }, + gravity: gpui::popup::PopupGravity::BottomRight, + constraint_adjustment: gpui::popup::PopupConstraintAdjustment::SLIDE_X + | gpui::popup::PopupConstraintAdjustment::SLIDE_Y + | gpui::popup::PopupConstraintAdjustment::FLIP_X + | gpui::popup::PopupConstraintAdjustment::FLIP_Y, + offset: point(px(0.0), px(0.0)), + // Only the outermost menu grabs input; a submenu is dismissed with its + // parent. + grab: depth == 0, + }; + let anchored_items = items.clone(); + let anchored_responder = responder.clone(); + let anchored = cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::new( + point(px(0.0), px(0.0)), + size(px(width), px(height)), + ))), + kind: WindowKind::AnchoredPopup(popup_options), + is_resizable: false, + ..Default::default() + }, + move |_window, cx| cx.new(|_| MenuPopup::new(anchored_items, anchored_responder, depth)), + ); + if let Ok(handle) = anchored { + let window: gpui::AnyWindowHandle = handle.into(); + log::info!("[gpui-menu] anchored popup {window:?} ({width}x{height})"); + return Ok(window); + } + + let origin = parent + .update(cx, |_root: gpui::AnyView, window, _cx| { + let bounds = window.bounds(); + ( + f32::from(bounds.origin.x) + anchor.0, + f32::from(bounds.origin.y) + anchor.1, + ) + }) + .map_err(|error| format!("the menu's window is gone: {error}"))?; + let floating = cx + .open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::new( + point(px(origin.0), px(origin.1)), + size(px(width), px(height)), + ))), + kind: WindowKind::PopUp, + is_resizable: false, + ..Default::default() + }, + move |_window, cx| cx.new(|_| MenuPopup::new(items, responder, depth)), + ) + .map_err(|error| format!("could not open the menu popup: {error}"))?; + let window: gpui::AnyWindowHandle = floating.into(); + log::info!("[gpui-menu] floating popup {window:?} at {origin:?}"); + Ok(window) +} + +fn popup_size(model: &MenuModel) -> (f32, f32) { + let mut rows = 0usize; + let mut widest = 0usize; + fn walk(items: &[MenuNode], rows: &mut usize, widest: &mut usize) { + for node in items { + *rows += 1; + match node { + MenuNode::Item(item) => { + let mut width = item.label.chars().count(); + if let Some(accelerator) = &item.accelerator { + width += accelerator.chars().count() + 4; + } + *widest = (*widest).max(width); + } + MenuNode::Submenu(submenu) => { + *widest = (*widest).max(submenu.label.chars().count() + 2); + } + _ => {} + } + } + } + walk(&model.items, &mut rows, &mut widest); + let width = (widest as f32 * 8.0 + PADDING * 2.0 + 28.0).clamp(MIN_WIDTH, MAX_WIDTH); + let height = (rows as f32 * ROW_HEIGHT + PADDING * 2.0).max(ROW_HEIGHT); + (width, height) +} + +pub struct MenuPopup { + items: Vec, + responder: Responder, + depth: usize, + /// Set when this popup resolved the request, so a parent popup does not + /// resolve it a second time. + resolved: bool, +} + +impl MenuPopup { + fn new(items: Vec, responder: Responder, depth: usize) -> Self { + Self { + items, + responder, + depth, + resolved: false, + } + } + + fn close(window: &mut Window) { + window.remove_window(); + } + + fn choose(&mut self, id: &str, window: &mut Window, cx: &mut App) { + if self.resolved { + return; + } + self.resolved = true; + self.responder.resolve(Some(id)); + self.forget_if_tracked(cx); + Self::close(window); + } + + /// Forgets the tracked root menu if this popup *is* it. + /// + /// Called from a root popup as it resolves so `PendingMenuGlobal` does not + /// keep a removed window: a later `close_open_menu` would then try to update + /// a window that is already gone, and `pending()` would report a menu that + /// is no longer on screen. A submenu (`depth > 0`) is not the tracked window + /// and leaves the root alone. + fn forget_if_tracked(&self, cx: &mut App) { + if self.depth != 0 { + return; + } + if let Some(pending) = cx.try_global::() { + *pending.0.borrow_mut() = None; + } + } + + pub fn pending(cx: &App) -> Option<(gpui::AnyWindowHandle, Vec)> { + cx.try_global::().and_then(|pending| { + pending + .0 + .borrow() + .as_ref() + .map(|menu| (menu.window, menu.items.clone())) + }) + } + + pub fn select(cx: &mut App, id: &str) -> bool { + let Some(pending) = cx.try_global::() else { + return false; + }; + let taken = pending.0.borrow_mut().take(); + let Some(menu) = taken else { + return false; + }; + menu.responder.resolve(Some(id)); + let _ = menu + .window + .update(cx, |_root, window, _cx| window.remove_window()); + true + } + + pub fn dismiss(cx: &mut App) -> bool { + let Some(pending) = cx.try_global::() else { + return false; + }; + let taken = pending.0.borrow_mut().take(); + let Some(menu) = taken else { + return false; + }; + menu.responder.resolve(None); + let _ = menu + .window + .update(cx, |_root, window, _cx| window.remove_window()); + true + } + + fn open_submenu( + &self, + items: Vec, + x: f32, + y: f32, + window: &mut Window, + cx: &mut Context, + ) { + let parent: gpui::AnyWindowHandle = window.window_handle(); + let responder = self.responder.clone(); + let depth = self.depth + 1; + // Deferred for the same reason as the menu bar's handler: this runs + // inside the popup window's update, and the submenu's fallback reads the + // parent's bounds with a re-entrant `parent.update(...)`. + cx.defer(move |cx| { + if let Err(error) = open_menu_window(cx, parent, (x, y), items, responder, depth) { + log::error!("[gpui-menu] {error}"); + } + }); + } +} + +impl Render for MenuPopup { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let mut rows: Vec = Vec::new(); + // One icon gives every row the same leading column, so the labels stay + // aligned. + let icon_column = self.items.iter().any(|node| match node { + MenuNode::Item(item) => item.icon.is_some(), + MenuNode::Submenu(submenu) => submenu.icon.is_some(), + _ => false, + }); + for (index, node) in self.items.clone().into_iter().enumerate() { + match node { + MenuNode::Separator => { + rows.push( + gpui::div() + .h(px(1.0)) + .mx_2() + .my_1() + .bg(gpui::rgba(0x00000022)) + .into_any_element(), + ); + } + MenuNode::StandardMenu(_) => {} + MenuNode::Item(item) => { + let id = item.id.clone(); + let enabled = item.enabled; + // A stable id is what gives the row a hitbox: without it the + // `hover` style has no state to key on and only ever applied + // to the first row the pointer entered. + let mut row = gpui::div() + .id(SharedString::from(format!("menu-item-{id}"))) + .flex() + .flex_row() + .items_center() + .justify_between() + .gap_4() + .px_2() + .h(px(ROW_HEIGHT)) + .text_size(px(13.0)) + .child( + gpui::div() + .flex() + .flex_row() + .items_center() + .gap_1() + .child(leading(item.checked, item.icon.as_deref(), icon_column)) + .child(item.label.clone()), + ) + .child( + gpui::div() + .text_color(gpui::rgba(0x00000088)) + .child(item.accelerator.clone().unwrap_or_default()), + ); + if !enabled { + row = row.text_color(gpui::rgba(0x00000066)); + } else { + row = row + .cursor_pointer() + .hover(|style| style.bg(gpui::rgba(0x00000011))) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener(move |this, _event, window, cx| { + this.choose(&id, window, cx); + }), + ); + } + let _ = index; + rows.push(row.into_any_element()); + } + MenuNode::Submenu(submenu) => { + let items = submenu.items.clone(); + let enabled = submenu.enabled; + // An id for the same reason as a menu item's row: the hover + // style needs a hitbox to track. + let mut row = gpui::div() + .id(SharedString::from(format!( + "menu-submenu-{}", + submenu.label + ))) + .flex() + .flex_row() + .items_center() + .justify_between() + .px_2() + .h(px(ROW_HEIGHT)) + .text_size(px(13.0)) + .child( + gpui::div() + .flex() + .flex_row() + .items_center() + .gap_1() + .when(icon_column, |label| { + label.child(leading(false, submenu.icon.as_deref(), true)) + }) + .child(submenu.label.clone()), + ) + .child("›"); + if enabled { + row = row + .cursor_pointer() + .hover(|style| style.bg(gpui::rgba(0x00000011))) + .on_mouse_down( + gpui::MouseButton::Left, + cx.listener( + move |this, event: &gpui::MouseDownEvent, window, cx| { + this.open_submenu( + items.clone(), + f32::from(event.position.x), + f32::from(event.position.y), + window, + cx, + ); + }, + ), + ); + } + rows.push(row.into_any_element()); + } + } + } + + let responder = self.responder.clone(); + let resolved = self.resolved; + let depth = self.depth; + let _ = window; + + gpui::div() + .flex() + .flex_col() + .py(px(PADDING)) + .bg(gpui::white()) + .border_1() + .border_color(gpui::rgba(0x00000033)) + .rounded(px(6.0)) + .shadow_md() + .text_color(gpui::rgb(0x18212f)) + .on_key_down(move |event, window, cx| { + if event.keystroke.key == "escape" { + if !resolved { + responder.resolve(None); + } + // A root popup is the one `PendingMenuGlobal` tracks; clear + // it so a later open/dismiss does not touch a removed window. + if depth == 0 { + if let Some(pending) = cx.try_global::() { + *pending.0.borrow_mut() = None; + } + } + window.remove_window(); + } + }) + .children(rows) + } +} + +const ICON_SIZE: f32 = 16.0; + +/// The slot before a row's label: its check mark, else its icon. With +/// `icon_column` the slot is icon-wide on every row. +fn leading(checked: bool, icon: Option<&str>, icon_column: bool) -> gpui::AnyElement { + let image = (!checked) + .then(|| icon.and_then(crate::desktop::icon::cached)) + .flatten() + .and_then(|icon| icon.image.clone()); + if !icon_column { + return gpui::div() + .child(if checked { "✓" } else { " " }) + .into_any_element(); + } + let slot = gpui::div() + .flex() + .items_center() + .justify_center() + .size(px(ICON_SIZE)); + match image { + Some(image) => slot + .child(gpui::img(image).size(px(ICON_SIZE))) + .into_any_element(), + None => slot + .child(if checked { "✓" } else { "" }) + .into_any_element(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::desktop::menu_model; + use serde_json::json; + + #[test] + fn hiding_one_windows_bar_leaves_the_others_alone() { + // The inset is per window: a hidden bar in one window must not shrink + // another window's surface, which is why this is a set of ids rather than + // one flag. + set_menu_bar_visible(7, false); + assert!(menu_bar_hidden(7)); + assert!(!menu_bar_hidden(8)); + set_menu_bar_visible(7, true); + assert!(!menu_bar_hidden(7)); + } + + #[test] + fn the_inset_follows_the_windows_own_visibility() { + // Both ids are put into a known state first: the hidden set is process + // wide, so another test may have hidden either of them already. + set_menu_bar_visible(9, true); + set_menu_bar_visible(10, true); + + set_menu_bar_visible(9, false); + assert_eq!(menu_bar_inset_for_window_id(9), 0.0); + assert_eq!( + menu_bar_inset_for_window_id(10), + if should_use_gpui_menu_bar() { + MENU_BAR_HEIGHT + } else { + 0.0 + }, + "hiding one window's bar must not shrink another window", + ); + set_menu_bar_visible(9, true); + } + + #[test] + fn popup_size_grows_with_the_items() { + let one = menu_model::parse_menu(&json!([{"id": "a", "label": "A"}])).expect("valid"); + let two = menu_model::parse_menu(&json!([ + {"id": "a", "label": "A"}, + {"type": "separator"}, + {"id": "b", "label": "B"} + ])) + .expect("valid"); + let (_, one_height) = popup_size(&one); + let (_, two_height) = popup_size(&two); + assert!(two_height > one_height); + } + + #[test] + fn popup_width_is_clamped() { + let wide = menu_model::parse_menu(&json!([{ + "id": "a", + "label": "a very long label that would otherwise make the popup enormous indeed" + }])) + .expect("valid"); + let (width, _) = popup_size(&wide); + assert_eq!(width, MAX_WIDTH); + } + + #[test] + fn the_edit_menu_spells_out_its_roles() { + let nodes = crate::desktop::app_menu::standard_menu_nodes( + crate::desktop::menu_model::MenuRole::EditMenu, + "Tester", + false, + ); + let ids: Vec = nodes + .iter() + .filter_map(|node| match node { + MenuNode::Item(item) => Some(item.id.clone()), + _ => None, + }) + .collect(); + assert!(ids.contains(&"role:copy".to_string())); + assert!(ids.contains(&"role:selectAll".to_string())); + let copy = nodes + .iter() + .find_map(|node| match node { + MenuNode::Item(item) if item.id == "role:copy" => Some(item.clone()), + _ => None, + }) + .expect("copy is in the Edit menu"); + assert_eq!(copy.accelerator.as_deref(), Some("secondary-c")); + } + + #[test] + fn the_app_menu_is_labelled_with_the_app_name() { + let nodes = crate::desktop::app_menu::standard_menu_nodes( + crate::desktop::menu_model::MenuRole::AppMenu, + "Tester", + false, + ); + let about = nodes + .iter() + .find_map(|node| match node { + MenuNode::Item(item) + if item.role == Some(crate::desktop::menu_model::MenuRole::About) => + { + Some(item.label.clone()) + } + _ => None, + }) + .expect("the app menu has About"); + assert_eq!(about, "About Tester"); + } + + #[test] + fn the_help_menu_only_grows_a_developer_group_in_a_dev_build() { + use crate::desktop::menu_model::MenuRole; + let without = crate::desktop::app_menu::standard_menu_nodes(MenuRole::HelpMenu, "T", false); + assert!(without.is_empty()); + let with = crate::desktop::app_menu::standard_menu_nodes(MenuRole::HelpMenu, "T", true); + assert_eq!(with.len(), 1); + match &with[0] { + MenuNode::Submenu(submenu) => { + assert_eq!(submenu.label, "Developer"); + assert_eq!(submenu.items.len(), 5); + } + other => panic!("expected a submenu, got {other:?}"), + } + } + + #[test] + fn the_menu_bar_inset_follows_the_mode() { + std::env::set_var("RN_GPUI_MENU_BAR", "gpui"); + assert_eq!(menu_bar_inset(), MENU_BAR_HEIGHT); + std::env::set_var("RN_GPUI_MENU_BAR", "native"); + assert_eq!(menu_bar_inset(), 0.0); + std::env::remove_var("RN_GPUI_MENU_BAR"); + } + + #[test] + fn the_mode_follows_the_environment() { + std::env::set_var("RN_GPUI_CONTEXT_MENU", "gpui"); + assert!(should_use_gpui()); + std::env::set_var("RN_GPUI_CONTEXT_MENU", "native"); + assert!(!should_use_gpui()); + std::env::remove_var("RN_GPUI_CONTEXT_MENU"); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/icon.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/icon.rs new file mode 100644 index 0000000000..f3f0d7aee7 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/icon.rs @@ -0,0 +1,257 @@ +//! Icons for the tray and native menu items, loaded from the URIs an `Image` +//! source resolves to. +//! +//! The native menu builders are synchronous, so an icon is loaded before the +//! menu that shows it is built: each entry point awaits [`preload_menu`] and the +//! builders read [`cached`]. + +use gpui::http_client::HttpClient; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; + +/// Far more distinct icons than any app's menus hold; past it the cache starts +/// over instead of growing. +const MAX_CACHED_ICONS: usize = 256; + +pub struct DecodedIcon { + pub rgba: Vec, + pub width: u32, + pub height: u32, + /// The file as loaded, for the menus GPUI draws. + pub image: Option>, +} + +static ICONS: OnceLock>>> = OnceLock::new(); +static HTTP_CLIENT: OnceLock> = OnceLock::new(); + +fn icons() -> &'static Mutex>> { + ICONS.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Called once the app runs; Metro asset URLs are fetched with its client. +pub fn install(cx: &gpui::App) { + let _ = HTTP_CLIENT.set(cx.http_client()); +} + +pub fn cached(uri: &str) -> Option> { + icons().lock().ok()?.get(uri).cloned() +} + +pub fn decode(bytes: Vec) -> Result { + let format = image::guess_format(&bytes).map_err(|error| error.to_string())?; + let rgba = image::load_from_memory_with_format(&bytes, format) + .map_err(|error| error.to_string())? + .to_rgba8(); + let (width, height) = rgba.dimensions(); + let format = match format { + image::ImageFormat::Png => Some(gpui::ImageFormat::Png), + image::ImageFormat::Jpeg => Some(gpui::ImageFormat::Jpeg), + image::ImageFormat::WebP => Some(gpui::ImageFormat::Webp), + image::ImageFormat::Gif => Some(gpui::ImageFormat::Gif), + image::ImageFormat::Bmp => Some(gpui::ImageFormat::Bmp), + image::ImageFormat::Tiff => Some(gpui::ImageFormat::Tiff), + image::ImageFormat::Ico => Some(gpui::ImageFormat::Ico), + _ => None, + }; + Ok(DecodedIcon { + rgba: rgba.into_raw(), + width, + height, + image: format.map(|format| Arc::new(gpui::Image::from_bytes(format, bytes))), + }) +} + +pub async fn load(uri: &str) -> Result, String> { + if let Some(icon) = cached(uri) { + return Ok(icon); + } + let client = HTTP_CLIENT + .get() + .cloned() + .ok_or_else(|| "the desktop shell is not running".to_owned())?; + let bytes = crate::image_loader::load_bytes(uri, client).await?; + let icon = Arc::new(decode(bytes).map_err(|error| format!("{uri}: {error}"))?); + if let Ok(mut icons) = icons().lock() { + if icons.len() >= MAX_CACHED_ICONS { + icons.clear(); + } + icons.insert(uri.to_owned(), icon.clone()); + } + Ok(icon) +} + +/// The app's own icon, `desktop.json`'s `icon`, as a dev run is told it through +/// `RN_GPUI_APP_ICON`, scaled to the largest size a tray or a title bar shows. +/// +/// Read once. `None` when the variable is unset — a packaged app, which has its +/// icon in its executable and bundle instead — or the file cannot be read. +pub fn app_icon() -> Option> { + static APP_ICON: OnceLock>> = OnceLock::new(); + APP_ICON + .get_or_init(|| { + let path = std::env::var_os("RN_GPUI_APP_ICON")?; + let bytes = match std::fs::read(&path) { + Ok(bytes) => bytes, + Err(error) => { + log::warn!("[desktop] app icon {}: {error}", path.to_string_lossy()); + return None; + } + }; + match decode(bytes) { + Ok(icon) => Some(Arc::new(fit_within(icon, APP_ICON_EDGE))), + Err(error) => { + log::warn!("[desktop] app icon {}: {error}", path.to_string_lossy()); + None + } + } + }) + .clone() +} + +/// A tray icon is drawn at 16 to 32 points: the system scales anything larger +/// down with a plain filter, which leaves a 512-pixel logo ragged. Scaling it +/// here, once, with a good one keeps it crisp. +const APP_ICON_EDGE: u32 = 64; + +/// `icon` scaled so that neither side is longer than `edge`, or unchanged when +/// it already is smaller. The compressed source (`image`) is dropped: menus that +/// draw an icon themselves read the file, and this copy is only ever pixels. +fn fit_within(icon: DecodedIcon, edge: u32) -> DecodedIcon { + if icon.width <= edge && icon.height <= edge { + return icon; + } + let Some(source) = image::RgbaImage::from_raw(icon.width, icon.height, icon.rgba.clone()) + else { + return icon; + }; + let scale = f64::from(edge) / f64::from(icon.width.max(icon.height)); + let width = ((f64::from(icon.width) * scale).round() as u32).max(1); + let height = ((f64::from(icon.height) * scale).round() as u32).max(1); + let resized = image::imageops::resize(&source, width, height, image::imageops::Lanczos3); + DecodedIcon { + rgba: resized.into_raw(), + width, + height, + image: None, + } +} + +/// Loads every icon a menu payload names. A missing or broken icon is logged +/// and the item is shown without one, as an `Image` with a bad source would be. +pub async fn preload_menu(items: &Value) { + let mut uris = Vec::new(); + collect_uris(items, &mut uris); + for uri in uris { + if let Err(error) = load(&uri).await { + log::warn!("[desktop] menu icon {error}"); + } + } +} + +/// For menus that arrive as props rather than through an async call: starts +/// loading the icons that are not cached yet, so the menu has them by the time +/// it opens. +pub fn prefetch_menu(items: &Value) { + static PENDING: OnceLock>> = OnceLock::new(); + let mut uris = Vec::new(); + collect_uris(items, &mut uris); + let Ok(mut pending) = PENDING.get_or_init(Default::default).lock() else { + return; + }; + uris.retain(|uri| cached(uri).is_none() && pending.insert(uri.clone())); + if uris.is_empty() { + return; + } + crate::desktop::post_to_main(Box::new(move |cx| { + cx.background_executor() + .spawn(async move { + for uri in uris { + if let Err(error) = load(&uri).await { + log::warn!("[desktop] menu icon {error}"); + } + if let Some(pending) = PENDING.get() { + if let Ok(mut pending) = pending.lock() { + pending.remove(&uri); + } + } + } + }) + .detach(); + })); +} + +fn collect_uris(value: &Value, out: &mut Vec) { + match value { + Value::Array(items) => items.iter().for_each(|item| collect_uris(item, out)), + Value::Object(fields) => { + if let Some(Value::String(uri)) = fields.get("icon") { + if !out.contains(uri) { + out.push(uri.clone()); + } + } + if let Some(items) = fields.get("items") { + collect_uris(items, out); + } + } + _ => {} + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn a_menu_names_each_icon_once() { + let mut uris = Vec::new(); + collect_uris( + &json!([ + {"id": "a", "label": "A", "icon": "file:///a.png"}, + {"type": "submenu", "label": "S", "icon": "file:///s.png", "items": [ + {"id": "b", "label": "B", "icon": "file:///a.png"}, + ]}, + {"type": "separator"}, + ]), + &mut uris, + ); + assert_eq!(uris, ["file:///a.png", "file:///s.png"]); + } + + #[test] + fn decoding_yields_rgba_pixels() { + let mut png = Vec::new(); + image::RgbaImage::from_pixel(3, 2, image::Rgba([1, 2, 3, 4])) + .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) + .unwrap(); + let icon = decode(png).unwrap(); + assert_eq!((icon.width, icon.height), (3, 2)); + assert_eq!(&icon.rgba[..4], &[1, 2, 3, 4]); + } + + fn solid(width: u32, height: u32) -> DecodedIcon { + DecodedIcon { + rgba: image::RgbaImage::from_pixel(width, height, image::Rgba([10, 20, 30, 255])) + .into_raw(), + width, + height, + image: None, + } + } + + #[test] + fn a_large_app_icon_is_scaled_down_keeping_its_proportions() { + let icon = fit_within(solid(512, 256), 64); + assert_eq!((icon.width, icon.height), (64, 32)); + assert_eq!(icon.rgba.len(), 64 * 32 * 4); + // A flat color stays that color through the filter. + assert_eq!(&icon.rgba[..4], &[10, 20, 30, 255]); + } + + #[test] + fn a_small_app_icon_is_left_alone() { + let icon = fit_within(solid(32, 32), 64); + assert_eq!((icon.width, icon.height), (32, 32)); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/keymap.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/keymap.rs new file mode 100644 index 0000000000..23724682db --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/keymap.rs @@ -0,0 +1,1151 @@ +//! Keyboard shortcuts, as ordinary keymap bindings. +//! +//! `useShortcut('mod+s', …)` in JS becomes one `KeyBinding` here, and a +//! `` becomes a `key_context` on the element core already builds +//! for that node. Nothing intercepts keystrokes: the binding is resolved by +//! GPUI's own keymap, from the focused element's dispatch path, so +//! +//! * a shortcut consumes the key *before* a `TextInput`'s input handler sees +//! it (`window.rs` `dispatch_keystroke` only reaches the handler when +//! `cx.propagate_event` is still true), +//! * a binding in a deeper scope outranks one in a shallower scope +//! (`keymap.rs` `binding_enabled` returns the depth the predicate matched at, +//! and `bindings_for_input` sorts by depth), +//! * an inner scope can override an outer one, because every matching binding +//! is returned in precedence order and dispatched until one stops +//! propagation. +//! +//! A menu accelerator is then just another binding — in the app-wide context — +//! so a component can beat it, which the old `intercept_keystrokes` path could +//! never allow. + +use gpui::{Action, App, KeyBinding}; +use serde_json::{json, Value}; + +use crate::store::store_from; + +/// The key context a `TextInput` puts on its element. A shortcut that does not +/// ask for `allowInTextInput` excludes itself with `!rn-text-input`. +pub const TEXT_INPUT_CONTEXT: &str = "rn-text-input"; + +/// The context the window root carries, so an app-scope shortcut can match with +/// nothing focused. +/// +/// This is not decoration. `KeyBindingContextPredicate::eval_inner` returns +/// false for an *empty* context stack before it ever reaches a `Not`, so a +/// binding like `!rn-text-input` matches nothing when the focus path produces no +/// contexts at all — which is exactly the state a key arrives in when the window +/// has no focused element. A positive identifier on the root gives every key a +/// context to match against, and it is the weakest one the keymap can have, +/// which is what "the window scope may be overridden" means. +pub const APP_CONTEXT: &str = "rn-app"; + +/// Prefix for a scope's context, so a scope named `save` cannot collide with a +/// context core or a library already uses. +const SCOPE_PREFIX: &str = "rn-scope-"; + +/// One shortcut, as JS declares it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Shortcut { + /// The stable name a handler is registered under, e.g. `file.save`. + pub id: String, + /// GPUI keystroke text, already normalised: `secondary-s`, or + /// `secondary-k secondary-s` for a chord. + pub keys: String, + /// The `ShortcutScope` it belongs to, or `None` for the window/app scope. + pub scope: Option, + pub enabled: bool, + pub allow_in_text_input: bool, +} + +impl Shortcut { + /// Whether `keystroke` is the (first, for a chord) key this shortcut binds. + pub fn key_matches(&self, keystroke: &gpui::Keystroke) -> bool { + match gpui::Keystroke::parse(&self.keys) { + Ok(parsed) => parsed.should_match(&gpui::KeybindingKeystroke::from_keystroke( + keystroke.clone(), + )), + Err(_) => false, + } + } + + /// The context predicate this shortcut's bindings carry. + /// + /// `depth_of` returns the deepest prefix of the context stack the predicate + /// matches, which is what gives an inner scope precedence over an outer one. + /// A shortcut outside a text input appends `!rn-text-input`: `Not` scans the + /// whole stack, so it fails whenever a text input is anywhere on the focus + /// path — the inner editing keys keep the key and no binding consumes it. + pub fn context(&self) -> String { + let scope = self.scope.as_deref().map(scope_context); + match (scope, self.allow_in_text_input) { + (Some(scope), false) => format!("{scope} && !{TEXT_INPUT_CONTEXT}"), + (Some(scope), true) => scope, + (None, false) => format!("{APP_CONTEXT} && !{TEXT_INPUT_CONTEXT}"), + // The window/app scope. `rn-app` is on the window root, so this + // matches with nothing focused — and it is the shallowest context, + // so every scope overrides it. + (None, true) => APP_CONTEXT.to_string(), + } + } +} + +/// The `KeyContext` identifier for a scope name. +/// +/// GPUI identifiers are alphanumeric, `_` and `-` (`keymap/context.rs` +/// `is_identifier_char`), so a name with anything else is folded to `-`. The +/// prefix keeps a scope from shadowing a context somebody else defines. +pub fn scope_context(name: &str) -> String { + let mut out = String::with_capacity(SCOPE_PREFIX.len() + name.len()); + out.push_str(SCOPE_PREFIX); + for character in name.chars() { + if character.is_alphanumeric() || character == '_' { + out.push(character); + } else { + out.push('-'); + } + } + out +} + +/// Parses a JS shortcut spec into GPUI keystroke text. +/// +/// One key is a `+`-separated list of modifiers and a key; a chord is those +/// separated by whitespace. The result folds the spellings JS uses onto the ones +/// GPUI parses, so `mod` works on every platform and `ArrowLeft` means the same +/// as `left`. +pub fn parse_shortcut(spec: &str) -> Result { + let spec = spec.trim(); + if spec.is_empty() { + return Err("a shortcut cannot be empty".to_string()); + } + let mut chord = Vec::new(); + for key in spec.split_whitespace() { + chord.push(parse_key(key)?); + } + Ok(chord.join(" ")) +} + +/// One keystroke of a chord, e.g. `mod+shift+k` -> `secondary-shift-k`. +fn parse_key(key: &str) -> Result { + let mut parts: Vec<&str> = key.split('+').map(str::trim).collect(); + let key_name = parts + .pop() + .filter(|part| !part.is_empty()) + .ok_or_else(|| format!("invalid shortcut key: {key}"))?; + // `mod++` means the plus key; the split above turned it into an empty last + // part, so put the character back. + let key_name = if key_name.is_empty() { "+" } else { key_name }; + + // A modifier listed twice is one modifier; `mod` and `ctrl` on Linux are the + // same key, so they collapse too. + let mut modifiers: Vec<&'static str> = Vec::new(); + for part in parts { + if part.is_empty() { + // `cmd++` again: the trailing `+` is part of the key, not a + // separator, so this is only an error when the key is not `+`. + if key_name == "+" { + continue; + } + return Err(format!("invalid shortcut key: {key}")); + } + let modifier = match part.to_ascii_lowercase().as_str() { + // `mod` is Command on macOS and Control elsewhere, which is what + // GPUI's `secondary` already means. + "mod" | "cmdorctrl" | "commandorcontrol" | "cmdorcontrol" | "secondary" => "secondary", + "cmd" | "command" | "super" | "meta" | "win" => "cmd", + "ctrl" | "control" => "ctrl", + "alt" | "option" | "opt" => "alt", + "shift" => "shift", + other => return Err(format!("unknown shortcut modifier: {other}")), + }; + if !modifiers.contains(&modifier) { + modifiers.push(modifier); + } + } + + let key = normalise_key(key_name).ok_or_else(|| format!("unknown shortcut key: {key_name}"))?; + + let mut out = String::new(); + // `secondary` first so a binding reads the way it is written, then the rest in + // a fixed order: two spellings of the same chord must produce one string, or + // the same shortcut would be installed twice. + for modifier in ["secondary", "cmd", "ctrl", "alt", "shift", "fn"] { + if modifiers.contains(&modifier) { + out.push_str(modifier); + out.push('-'); + } + } + out.push_str(&key); + Ok(out) +} + +/// The GPUI key name for a JS or Electron spelling. +/// +/// GPUI keeps the lower-case name (`keystroke.key`) and only compares it; the +/// web names in `onKeyDown` are produced separately, in `normalise_web_key`. +fn normalise_key(token: &str) -> Option { + let lower = token.to_ascii_lowercase(); + if token.chars().count() == 1 { + let character = token.chars().next()?; + if character.is_ascii_alphanumeric() { + return Some(lower); + } + // Punctuation goes through as-is: GPUI parses it back, and `-` is + // handled by its own splitting rule. + return Some(character.to_string()); + } + let named = match lower.as_str() { + "enter" | "return" => "enter", + "escape" | "esc" => "escape", + "tab" => "tab", + "space" | "spacebar" | " " => "space", + "backspace" => "backspace", + "delete" | "del" => "delete", + "insert" | "ins" => "insert", + "up" | "arrowup" => "up", + "down" | "arrowdown" => "down", + "left" | "arrowleft" => "left", + "right" | "arrowright" => "right", + "home" => "home", + "end" => "end", + "pageup" | "page-up" => "pageup", + "pagedown" | "page-down" => "pagedown", + "comma" => ",", + "period" | "dot" => ".", + "slash" => "/", + "backslash" => "\\", + "semicolon" => ";", + "quote" | "apostrophe" => "'", + "backquote" => "`", + "bracketleft" => "[", + "bracketright" => "]", + "minus" => "-", + "plus" => "+", + "equal" => "=", + _ => { + // Function keys, `f1`..`f24`. GPUI has no list of valid names, so this + // only has to agree with what the platform reports. + if let Some(number) = lower.strip_prefix('f') { + if let Ok(number) = number.parse::() { + if (1..=24).contains(&number) { + return Some(format!("f{number}")); + } + } + } + return None; + } + }; + Some(named.to_string()) +} + +/// The web `key` name for a GPUI key, so `onKeyDown` reports `ArrowLeft` and +/// not `left`. +/// +/// `event.key` in the DOM is a single printable character for printable keys or +/// one of the named values; `Shift+A` is `"A"`, which needs the modifiers to say. +pub fn normalise_web_key(key: &str, shift: bool) -> String { + let named = match key { + "enter" => "Enter", + "escape" => "Escape", + "tab" => "Tab", + "space" => " ", + "backspace" => "Backspace", + "delete" => "Delete", + "insert" => "Insert", + "up" => "ArrowUp", + "down" => "ArrowDown", + "left" => "ArrowLeft", + "right" => "ArrowRight", + "home" => "Home", + "end" => "End", + "pageup" => "PageUp", + "pagedown" => "PageDown", + // `capslock` and friends: capitalise the first letter so a DOM consumer + // sees `CapsLock` rather than the GPUI spelling. + other => { + if other.chars().count() == 1 { + let character = other.chars().next().unwrap_or_default(); + return if shift { + character.to_uppercase().to_string() + } else { + character.to_string() + }; + } + let mut characters = other.chars(); + return match characters.next() { + Some(first) => first.to_uppercase().collect::() + characters.as_str(), + None => String::new(), + }; + } + }; + named.to_string() +} + +/// The DOM `code` for a GPUI key: the physical key, independent of layout and +/// of Shift. +pub fn web_code(key: &str) -> String { + match key { + "enter" => "Enter".to_string(), + "escape" => "Escape".to_string(), + "tab" => "Tab".to_string(), + "space" => "Space".to_string(), + "backspace" => "Backspace".to_string(), + "delete" => "Delete".to_string(), + "insert" => "Insert".to_string(), + "up" => "ArrowUp".to_string(), + "down" => "ArrowDown".to_string(), + "left" => "ArrowLeft".to_string(), + "right" => "ArrowRight".to_string(), + "home" => "Home".to_string(), + "end" => "End".to_string(), + "pageup" => "PageUp".to_string(), + "pagedown" => "PageDown".to_string(), + other => { + if other.chars().count() == 1 { + let character = other.chars().next().unwrap_or_default(); + return match character { + 'a'..='z' => format!("Key{}", character.to_ascii_uppercase()), + 'A'..='Z' => format!("Key{}", character.to_ascii_uppercase()), + '0'..='9' => format!("Digit{character}"), + ' ' => "Space".to_string(), + ',' => "Comma".to_string(), + '.' => "Period".to_string(), + '/' => "Slash".to_string(), + '\\' => "Backslash".to_string(), + ';' => "Semicolon".to_string(), + '\'' => "Quote".to_string(), + '`' => "Backquote".to_string(), + '[' => "BracketLeft".to_string(), + ']' => "BracketRight".to_string(), + '-' => "Minus".to_string(), + '=' => "Equal".to_string(), + // Not a physical key name the DOM defines; an empty string is + // what the DOM reports for a key it cannot name. + _ => String::new(), + }; + } + if let Some(number) = other.strip_prefix('f') { + if number.parse::().is_ok() { + return format!("F{number}"); + } + } + String::new() + } + } +} + +/// The legacy `keyCode` for a GPUI key, or 0 when the DOM has none. +pub fn key_code(key: &str) -> u32 { + match key { + "backspace" => 8, + "tab" => 9, + "enter" => 13, + "escape" => 27, + "space" => 32, + "pageup" => 33, + "pagedown" => 34, + "end" => 35, + "home" => 36, + "left" => 37, + "up" => 38, + "right" => 39, + "down" => 40, + "insert" => 45, + "delete" => 46, + other => { + if key.len() == 1 { + let character = key.as_bytes()[0]; + if character.is_ascii_lowercase() { + return u32::from(character.to_ascii_uppercase()); + } + if character.is_ascii_digit() { + return u32::from(character); + } + } + if let Some(number) = other.strip_prefix('f') { + if let Ok(number) = number.parse::() { + if (1..=24).contains(&number) { + return 111 + number; + } + } + } + // Punctuation: the DOM's `keyCode` is the ASCII code for the + // unshifted character, which is what we already have. + other + .chars() + .next() + .filter(|character| character.is_ascii()) + .map(|character| u32::from(character as u8)) + .unwrap_or(0) + } + } +} + +/// The shortcuts in a JSON array, as `GpuiShortcuts` installs them. +/// +/// A malformed entry is skipped with a log line rather than failing the whole +/// install: one bad shortcut must not leave an app with no working keys. +pub fn parse_shortcuts(value: &Value) -> Result, String> { + let array = value + .as_array() + .ok_or_else(|| "shortcuts must be an array".to_string())?; + let mut shortcuts = Vec::with_capacity(array.len()); + for entry in array { + let Some(object) = entry.as_object() else { + log::warn!("[shortcut] skipped a shortcut that is not an object"); + continue; + }; + let Some(id) = object.get("id").and_then(Value::as_str) else { + log::warn!("[shortcut] skipped a shortcut with no `id`"); + continue; + }; + let Some(keys) = object.get("keys").and_then(Value::as_str) else { + log::warn!("[shortcut] {id} has no `keys`; skipped"); + continue; + }; + let keys = match parse_shortcut(keys) { + Ok(keys) => keys, + Err(error) => { + log::warn!("[shortcut] {id} has invalid keys: {error}"); + continue; + } + }; + shortcuts.push(Shortcut { + id: id.to_string(), + keys, + scope: object + .get("scope") + .and_then(Value::as_str) + .map(str::to_string), + enabled: object + .get("enabled") + .and_then(Value::as_bool) + .unwrap_or(true), + allow_in_text_input: object + .get("allowInTextInput") + .and_then(Value::as_bool) + .unwrap_or(false), + }); + } + Ok(shortcuts) +} + +/// The installed shortcuts as JSON, which is what `shortcutGet` answers with. +pub fn shortcuts_json(cx: &App) -> Value { + Value::Array( + shortcuts(cx) + .into_iter() + .map(|shortcut| { + json!({ + "id": shortcut.id, + "keys": shortcut.keys, + "scope": shortcut.scope, + "enabled": shortcut.enabled, + "allowInTextInput": shortcut.allow_in_text_input, + }) + }) + .collect(), + ) +} + +/// A shortcut's action, carrying the id JS registered it under. +#[derive(Clone, Debug, PartialEq, gpui::Action)] +#[action(namespace = rn_gpui_shortcut, no_json, no_register)] +pub struct ShortcutAction { + pub id: String, +} + +/// The installed shortcut list, and what the keymap was last built from. +pub struct ShortcutState { + shortcuts: Vec, + /// The signature the current keymap bindings were built from. Rebuilding is + /// only worth doing when this changes: a re-render must not thrash the + /// keymap. + built: String, + /// A rebuild was asked for and a deferred one is already waiting. + scheduled: bool, +} + +impl Default for ShortcutState { + fn default() -> Self { + Self { + shortcuts: Vec::new(), + built: String::new(), + scheduled: false, + } + } +} + +pub struct ShortcutGlobal(pub ShortcutState); +impl gpui::Global for ShortcutGlobal {} + +impl Default for ShortcutGlobal { + fn default() -> Self { + Self(ShortcutState::default()) + } +} + +/// Installs a shortcut list, rebuilding the keymap only if it changed. +/// +/// `scope_tags` is the set of mounted scope tags in the focused surface: a +/// binding is only useful where its scope element exists, so this is part of the +/// signature that decides whether to rebuild. +pub fn install(cx: &mut App, shortcuts: Vec) { + let signature = signature(&shortcuts); + let state = &mut ensure_global(cx).0; + if state.built == signature { + return; + } + state.shortcuts = shortcuts; + schedule_rebuild(cx); +} + +/// The shortcuts currently installed. +pub fn shortcuts(cx: &App) -> Vec { + cx.try_global::() + .map(|state| state.0.shortcuts.clone()) + .unwrap_or_default() +} + +/// The ids that belong to `scope` (or to no scope, when `scope` is `None`). +pub fn ids_in_scope(cx: &App, scope: Option<&str>) -> Vec { + shortcuts(cx) + .into_iter() + .filter(|shortcut| shortcut.enabled && shortcut.scope.as_deref() == scope) + .map(|shortcut| shortcut.id) + .collect() +} + +/// Whether `id` is installed and enabled anywhere, so a `window`-scope action +/// from an element can be told apart from one it should pass on. +pub fn is_installed(cx: &App, id: &str) -> bool { + cx.try_global::().is_some_and(|state| { + state + .0 + .shortcuts + .iter() + .any(|shortcut| shortcut.enabled && shortcut.id == id) + }) +} + +/// Whether a scoped shortcut would consume `keystroke` from the focused element. +/// +/// Used by the menu interceptor, which runs before the keymap is consulted: it +/// has to know whether a shortcut outranks the menu accelerator it is about to +/// handle, or it would take the key away from a shortcut that is entitled to it. +/// +/// `window.context_stack()` is the public way to ask what the focused element's +/// context is, so the precedence is evaluated here the same way +/// `Keymap::bindings_for_input` does it: deepest matching context first, later +/// bindings before earlier ones. +/// +/// Only a *scoped* binding counts. An app-scope shortcut and a menu accelerator +/// both live in the window root's context, where the menu keeps the key — which +/// is what it did before this work, so nothing changes for the window scope. +pub fn claims_keystroke(cx: &App, window: &gpui::Window, keystroke: &gpui::Keystroke) -> bool { + let stack = window.context_stack(); + // The menu accelerator this key would trigger is in the app context, which + // is the shallowest one. A shortcut that matches at a greater depth wins the + // key; a tie does not, so the menu keeps its behaviour there. + let app_depth = gpui::KeyBindingContextPredicate::parse(APP_CONTEXT) + .ok() + .and_then(|predicate| predicate.depth_of(&stack)) + .unwrap_or(0); + shortcuts(cx).iter().any(|shortcut| { + if !shortcut.enabled || shortcut.scope.is_none() { + return false; + } + // The shortcut's *own* context, not a guessed one: a shortcut that opted + // into text inputs has no `!rn-text-input`, and evaluating anything else + // here would let an Edit-menu accelerator keep a key the shortcut is + // entitled to (`mod+a` and `selectAll` are exactly that pair). + let Some(depth) = gpui::KeyBindingContextPredicate::parse(&shortcut.context()) + .ok() + .and_then(|predicate| predicate.depth_of(&stack)) + else { + return false; + }; + depth > app_depth && shortcut.key_matches(keystroke) + }) +} + +/// Whether the shortcut `id` belongs to `scope`, so a scope element knows if an +/// action reaching it is its own or should be passed outward. +pub fn scope_owns(cx: &App, scope: &str, id: &str) -> bool { + cx.try_global::().is_some_and(|state| { + state.0.shortcuts.iter().any(|shortcut| { + shortcut.enabled && shortcut.id == id && shortcut.scope.as_deref() == Some(scope) + }) + }) +} + +/// Sends `topShortcut` to `tag`, which is the JS node for the scope that owns +/// the action. +pub fn dispatch_to(cx: &mut App, tag: i32, id: &str, window: Option<&mut gpui::Window>) { + let Some(store) = store_from(cx) else { + return; + }; + let host = store.read(cx).host.clone(); + host.dispatch_event(tag, "topShortcut", &json!({ "id": id })); + if let Some(window) = window { + store.read(cx).frame_hook.arm(window); + } +} + +/// Sends `gpuiDesktop:shortcut` for a shortcut with no scope element, which is +/// the window/app scope and any shortcut whose scope has not mounted. +fn emit_app_shortcut(cx: &mut App, id: &str, scope: Option<&str>) { + let Some(store) = store_from(cx) else { + return; + }; + let host = store.read(cx).host.clone(); + host.emit_device_event_json( + "gpuiDesktop:shortcut", + &json!({ "id": id, "scope": scope }).to_string(), + ); +} + +fn ensure_global(cx: &mut App) -> &mut ShortcutGlobal { + if cx.try_global::().is_none() { + cx.set_global(ShortcutGlobal::default()); + } + cx.global_mut::() +} + +/// A stable description of a shortcut list, so an unchanged list costs nothing. +fn signature(shortcuts: &[Shortcut]) -> String { + let mut rows: Vec = shortcuts + .iter() + .map(|shortcut| { + format!( + "{}|{}|{}|{}|{}", + shortcut.id, + shortcut.keys, + shortcut.scope.as_deref().unwrap_or(""), + shortcut.enabled, + shortcut.allow_in_text_input + ) + }) + .collect(); + // Order is not meaning: two lists with the same shortcuts are the same + // keymap, whichever order JS happened to register them in. Sorted so a + // re-render that reorders the array does not rebuild. + rows.sort(); + rows.join("\n") +} + +/// Rebuilds once the current update has finished. +/// +/// Debounced because a render pass can install shortcuts several times — a +/// mount batch, a props update, a user keymap change — and each rebuild clears +/// and re-adds every binding in the app. +/// +/// Deferred on the app, not on a window's next frame: GPUI runs a window's +/// next-frame callbacks from the platform's frame request, and a hidden or +/// offscreen window (every e2e window on Windows) never gets one, so the +/// bindings would never be installed. +fn schedule_rebuild(cx: &mut App) { + if cx + .try_global::() + .is_some_and(|state| state.0.scheduled) + { + return; + } + ensure_global(cx).0.scheduled = true; + cx.defer(|cx| { + if cx + .try_global::() + .is_some_and(|state| state.0.scheduled) + { + rebuild_now(cx); + } + }); +} + +/// Replaces every `ShortcutAction` binding in the keymap with the current list. +fn rebuild_now(cx: &mut App) { + let shortcuts = cx + .try_global::() + .map(|state| state.0.shortcuts.clone()) + .unwrap_or_default(); + let signature = signature(&shortcuts); + + // `Keymap` has no per-binding removal, so the shortcut bindings are filtered + // out and the rest put back — the same approach the menu accelerators use. + let keymap = cx.key_bindings(); + let kept: Vec = keymap + .borrow() + .bindings() + .filter(|binding| !is_shortcut_action(binding.action())) + .cloned() + .collect(); + { + let mut keymap = keymap.borrow_mut(); + keymap.clear(); + keymap.add_bindings(kept); + } + + let mut bindings = Vec::with_capacity(shortcuts.len()); + for shortcut in &shortcuts { + if !shortcut.enabled { + continue; + } + // `KeyBinding::new` panics on a malformed context, and a scope name is + // user data, so the predicate is parsed here and a bad one skips the + // binding rather than taking the app down. + let predicate = { + let context = shortcut.context(); + if context.is_empty() { + None + } else { + match gpui::KeyBindingContextPredicate::parse(&context) { + Ok(predicate) => Some(std::rc::Rc::new(predicate)), + Err(error) => { + log::warn!( + "[shortcut] {} has an invalid context {context:?}: {error}", + shortcut.id + ); + continue; + } + } + } + }; + let Ok(binding) = KeyBinding::load( + &shortcut.keys, + Box::new(ShortcutAction { + id: shortcut.id.clone(), + }), + predicate, + false, + None, + &gpui::DummyKeyboardMapper, + ) else { + log::warn!( + "[shortcut] {} has unparseable keys {:?}; skipped", + shortcut.id, + shortcut.keys + ); + continue; + }; + bindings.push(binding); + } + cx.bind_keys(bindings); + + let state = &mut ensure_global(cx).0; + state.built = signature; + state.scheduled = false; +} + +fn is_shortcut_action(action: &dyn Action) -> bool { + action.as_any().downcast_ref::().is_some() +} + +/// Installs the app-wide action handler. +/// +/// This is the last stop: it runs in the bubble phase after every element on the +/// focus path had a chance to claim the action, so a scoped shortcut never +/// reaches it. Anything that does is in the window/app scope. +pub fn register(cx: &mut App) { + ensure_global(cx); + cx.on_action(|action: &ShortcutAction, cx: &mut App| { + let id = action.id.clone(); + let scope = shortcuts(cx) + .into_iter() + .find(|shortcut| shortcut.id == id) + .and_then(|shortcut| shortcut.scope); + // A scoped shortcut that reached this handler has no scope element on + // the focus path, so there is no node to report to: it goes out as the + // device event, which the registry also listens to. + emit_app_shortcut(cx, &id, scope.as_deref()); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui::Keystroke; + + #[test] + fn mod_is_secondary_on_every_platform() { + // `secondary` is what GPUI maps to Command on macOS and Control + // elsewhere, so one JS spelling works everywhere. + assert_eq!(parse_shortcut("mod+s").unwrap(), "secondary-s"); + assert_eq!( + parse_shortcut("CmdOrCtrl+Shift+K").unwrap(), + "secondary-shift-k" + ); + } + + #[test] + fn modifier_spellings_normalise() { + assert_eq!(parse_shortcut("Cmd+S").unwrap(), "cmd-s"); + assert_eq!(parse_shortcut("Meta+S").unwrap(), "cmd-s"); + assert_eq!(parse_shortcut("Ctrl+S").unwrap(), "ctrl-s"); + assert_eq!(parse_shortcut("Control+S").unwrap(), "ctrl-s"); + assert_eq!(parse_shortcut("Alt+S").unwrap(), "alt-s"); + assert_eq!(parse_shortcut("Option+S").unwrap(), "alt-s"); + // Order in the spec does not change the binding. + assert_eq!(parse_shortcut("Shift+Mod+K").unwrap(), "secondary-shift-k"); + assert_eq!(parse_shortcut("mod+shift+K").unwrap(), "secondary-shift-k"); + } + + #[test] + fn a_repeated_modifier_is_one_modifier() { + assert_eq!(parse_shortcut("mod+mod+s").unwrap(), "secondary-s"); + assert_eq!(parse_shortcut("Shift+shift+K").unwrap(), "shift-k"); + } + + #[test] + fn keys_fold_to_gpui_names() { + assert_eq!(parse_shortcut("Escape").unwrap(), "escape"); + assert_eq!(parse_shortcut("Esc").unwrap(), "escape"); + assert_eq!(parse_shortcut("ArrowLeft").unwrap(), "left"); + assert_eq!(parse_shortcut("Return").unwrap(), "enter"); + assert_eq!(parse_shortcut("Space").unwrap(), "space"); + assert_eq!(parse_shortcut("PageDown").unwrap(), "pagedown"); + assert_eq!(parse_shortcut("F5").unwrap(), "f5"); + assert_eq!(parse_shortcut("Cmd+,").unwrap(), "cmd-,"); + } + + #[test] + fn chords_keep_their_keys_in_order() { + assert_eq!( + parse_shortcut("mod+k mod+s").unwrap(), + "secondary-k secondary-s" + ); + assert_eq!( + parse_shortcut(" ctrl+x ctrl+y ").unwrap(), + "ctrl-x ctrl-y" + ); + } + + #[test] + fn bad_shortcuts_are_rejected_rather_than_panicking() { + assert!(parse_shortcut("").is_err()); + assert!(parse_shortcut(" ").is_err()); + assert!(parse_shortcut("Hyper+S").is_err()); + assert!(parse_shortcut("mod+UnknownKey").is_err()); + assert!(parse_shortcut("mod+f99").is_err()); + } + + #[test] + fn a_scope_becomes_a_prefixed_context() { + assert_eq!(scope_context("editor"), "rn-scope-editor"); + // Anything outside GPUI's identifier alphabet folds to `-`, so a scope + // named `a.b c` cannot produce a context that fails to parse. + assert_eq!(scope_context("a.b c"), "rn-scope-a-b-c"); + let parsed = gpui::KeyContext::parse(&scope_context("file view")).unwrap(); + assert!(parsed.contains("rn-scope-file-view")); + } + + #[test] + fn a_scoped_shortcut_excludes_text_inputs() { + let shortcut = Shortcut { + id: "file.save".to_string(), + keys: "secondary-s".to_string(), + scope: Some("editor".to_string()), + enabled: true, + allow_in_text_input: false, + }; + assert_eq!(shortcut.context(), "rn-scope-editor && !rn-text-input"); + + let allowed = Shortcut { + allow_in_text_input: true, + ..shortcut.clone() + }; + assert_eq!(allowed.context(), "rn-scope-editor"); + } + + #[test] + fn the_window_scope_has_no_predicate() { + let shortcut = Shortcut { + id: "app.quit".to_string(), + keys: "secondary-q".to_string(), + scope: None, + enabled: true, + allow_in_text_input: true, + }; + // The root context, which is the weakest binding in the keymap: any + // scope overrides it. + assert_eq!(shortcut.context(), APP_CONTEXT); + + let excluded = Shortcut { + allow_in_text_input: false, + ..shortcut + }; + assert_eq!( + excluded.context(), + format!("{APP_CONTEXT} && !{TEXT_INPUT_CONTEXT}") + ); + } + + /// A window-scope shortcut must fire with nothing focused. + /// + /// This is the bug the e2e caught and this unit test pins: an `empty` + /// context stack matches *nothing*, including a negation, so an app-scope + /// binding written as `!rn-text-input` could never fire before the root + /// carried `rn-app`. The predicate is evaluated against an empty stack here + /// on purpose, because that is what a window with no focused element + /// produces. + #[test] + fn the_window_scope_matches_with_nothing_focused() { + let shortcut = Shortcut { + id: "window.save".to_string(), + keys: "secondary-s".to_string(), + scope: None, + enabled: true, + allow_in_text_input: false, + }; + let binding = KeyBinding::new( + &shortcut.keys, + ShortcutAction { + id: shortcut.id.clone(), + }, + Some(&shortcut.context()), + ); + let mut keymap = gpui::Keymap::default(); + keymap.add_bindings([binding]); + let keystroke = Keystroke::parse("secondary-s").unwrap(); + + // No root context at all: nothing matches. This is why `rn-app` exists. + let (bindings, _) = keymap.bindings_for_input(&[keystroke.clone()], &[]); + assert!( + bindings.is_empty(), + "an empty context stack matches no predicate, not even a negation" + ); + + // The window root provides `rn-app`, and now the shortcut resolves. + let root = vec![gpui::KeyContext::parse(APP_CONTEXT).unwrap()]; + let (bindings, _) = keymap.bindings_for_input(&[keystroke], &root); + assert_eq!(bindings.len(), 1); + } + + /// The root context must not outrank a scope: it is the shallowest one. + #[test] + fn a_scope_outranks_the_app_context() { + let window = KeyBinding::new( + "secondary-j", + ShortcutAction { + id: "window".to_string(), + }, + Some(APP_CONTEXT), + ); + let scoped = KeyBinding::new( + "secondary-j", + ShortcutAction { + id: "scoped".to_string(), + }, + Some("rn-scope-editor"), + ); + let mut keymap = gpui::Keymap::default(); + keymap.add_bindings([window, scoped]); + let contexts = vec![ + gpui::KeyContext::parse(APP_CONTEXT).unwrap(), + gpui::KeyContext::parse("rn-scope-editor").unwrap(), + ]; + let (bindings, _) = + keymap.bindings_for_input(&[Keystroke::parse("secondary-j").unwrap()], &contexts); + assert_eq!( + bindings[0] + .action() + .as_any() + .downcast_ref::() + .unwrap() + .id, + "scoped" + ); + } + + /// The precedence rule the whole design rests on: a binding in a deeper + /// context matches at a greater depth, and `bindings_for_input` returns the + /// bindings sorted by it. + #[test] + fn an_inner_scope_outranks_an_outer_one() { + let outer = KeyBinding::new( + "secondary-s", + ShortcutAction { + id: "outer".to_string(), + }, + Some("rn-scope-outer"), + ); + let inner = KeyBinding::new( + "secondary-s", + ShortcutAction { + id: "inner".to_string(), + }, + Some("rn-scope-inner"), + ); + let mut keymap = gpui::Keymap::default(); + keymap.add_bindings([outer.clone(), inner.clone()]); + + let contexts = vec![ + gpui::KeyContext::parse("rn-scope-outer").unwrap(), + gpui::KeyContext::parse("rn-scope-inner").unwrap(), + ]; + let (bindings, pending) = + keymap.bindings_for_input(&[Keystroke::parse("secondary-s").unwrap()], &contexts); + assert!(!pending); + assert_eq!(bindings.len(), 2); + // Innermost first: dispatch runs them in this order and stops at the + // first one that consumes the key. + assert_eq!( + bindings[0] + .action() + .as_any() + .downcast_ref::() + .unwrap() + .id, + "inner" + ); + } + + /// A shortcut that excludes text inputs does not match when a text input is + /// on the focus path, so the input's own editing keys keep the key. + #[test] + fn a_text_input_disables_a_shortcut_that_opts_out() { + let binding = KeyBinding::new( + "secondary-a", + ShortcutAction { + id: "select.all".to_string(), + }, + Some(&format!("rn-scope-editor && !{TEXT_INPUT_CONTEXT}")), + ); + let mut keymap = gpui::Keymap::default(); + keymap.add_bindings([binding]); + + let in_editor = vec![ + gpui::KeyContext::parse("rn-scope-editor").unwrap(), + gpui::KeyContext::parse("rn-text-input").unwrap(), + ]; + let (bindings, _) = + keymap.bindings_for_input(&[Keystroke::parse("secondary-a").unwrap()], &in_editor); + assert!( + bindings.is_empty(), + "a text input must keep a key the shortcut opted out of" + ); + + let outside = vec![gpui::KeyContext::parse("rn-scope-editor").unwrap()]; + let (bindings, _) = + keymap.bindings_for_input(&[Keystroke::parse("secondary-a").unwrap()], &outside); + assert_eq!(bindings.len(), 1); + } + + #[test] + fn a_chord_needs_both_keys() { + let binding = KeyBinding::new( + "secondary-k secondary-s", + ShortcutAction { + id: "chord".to_string(), + }, + None, + ); + let mut keymap = gpui::Keymap::default(); + keymap.add_bindings([binding]); + let first = Keystroke::parse("secondary-k").unwrap(); + let second = Keystroke::parse("secondary-s").unwrap(); + + let (bindings, pending) = keymap.bindings_for_input(&[first.clone()], &[]); + assert!(bindings.is_empty()); + assert!(pending, "the first key must wait for the second"); + + let (bindings, pending) = keymap.bindings_for_input(&[first, second], &[]); + assert_eq!(bindings.len(), 1); + assert!(!pending); + } + + /// The six keys the old `dispatch_key` translated, plus the ones it did not. + /// + /// This is the stage-4 contract: every key gets a DOM `key`, a `code` and a + /// `keyCode`, where before only six names were mapped and everything else + /// went out as GPUI's spelling with `keyCode: 0`. + #[test] + fn every_key_gets_a_dom_name_code_and_key_code() { + for (gpui_key, web_key, code, legacy_code) in [ + ("left", "ArrowLeft", "ArrowLeft", 37), + ("up", "ArrowUp", "ArrowUp", 38), + ("right", "ArrowRight", "ArrowRight", 39), + ("down", "ArrowDown", "ArrowDown", 40), + ("enter", "Enter", "Enter", 13), + ("escape", "Escape", "Escape", 27), + ("backspace", "Backspace", "Backspace", 8), + ("delete", "Delete", "Delete", 46), + ("tab", "Tab", "Tab", 9), + ("space", " ", "Space", 32), + ("home", "Home", "Home", 36), + ("end", "End", "End", 35), + ("pageup", "PageUp", "PageUp", 33), + ("pagedown", "PageDown", "PageDown", 34), + ("a", "a", "KeyA", 65), + ("z", "z", "KeyZ", 90), + ("5", "5", "Digit5", 53), + ("f2", "F2", "F2", 113), + ] { + assert_eq!( + normalise_web_key(gpui_key, false), + web_key, + "key for {gpui_key}" + ); + assert_eq!(web_code(gpui_key), code, "code for {gpui_key}"); + assert_eq!(key_code(gpui_key), legacy_code, "keyCode for {gpui_key}"); + } + } + + #[test] + fn web_key_names_are_the_dom_ones() { + assert_eq!(normalise_web_key("left", false), "ArrowLeft"); + assert_eq!(normalise_web_key("up", false), "ArrowUp"); + assert_eq!(normalise_web_key("enter", false), "Enter"); + assert_eq!(normalise_web_key("escape", false), "Escape"); + assert_eq!(normalise_web_key("space", false), " "); + assert_eq!(normalise_web_key("a", false), "a"); + assert_eq!(normalise_web_key("a", true), "A"); + assert_eq!(normalise_web_key("f5", false), "F5"); + } + + #[test] + fn web_codes_are_physical_keys() { + assert_eq!(web_code("a"), "KeyA"); + assert_eq!(web_code("4"), "Digit4"); + assert_eq!(web_code("left"), "ArrowLeft"); + assert_eq!(web_code("space"), "Space"); + assert_eq!(web_code("f5"), "F5"); + } + + #[test] + fn key_codes_are_the_legacy_ones() { + assert_eq!(key_code("a"), 65); + assert_eq!(key_code("z"), 90); + assert_eq!(key_code("4"), 52); + assert_eq!(key_code("enter"), 13); + assert_eq!(key_code("escape"), 27); + assert_eq!(key_code("space"), 32); + assert_eq!(key_code("left"), 37); + assert_eq!(key_code("delete"), 46); + assert_eq!(key_code("f1"), 112); + assert_eq!(key_code("f12"), 123); + } + + #[test] + fn the_signature_ignores_order_but_not_content() { + let one = Shortcut { + id: "a".to_string(), + keys: "secondary-a".to_string(), + scope: None, + enabled: true, + allow_in_text_input: false, + }; + let two = Shortcut { + id: "b".to_string(), + keys: "secondary-b".to_string(), + scope: Some("editor".to_string()), + enabled: true, + allow_in_text_input: false, + }; + assert_eq!( + signature(&[one.clone(), two.clone()]), + signature(&[two.clone(), one.clone()]) + ); + assert_ne!(signature(&[one.clone()]), signature(&[two])); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/menu_events.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/menu_events.rs new file mode 100644 index 0000000000..2edd370469 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/menu_events.rs @@ -0,0 +1,236 @@ +//! One route for every `muda` menu event. +//! +//! `muda` has exactly one `MenuEvent` channel, and `MenuEvent::set_event_handler` +//! takes it over: its documentation is explicit that a handler installed with +//! `Some` means `MenuEvent::receiver()` **stops receiving events at all**. The +//! setter is backed by a `OnceLock`, so the first caller wins and every later +//! call is silently ignored. +//! +//! Three parts of this app build `muda` menus — the Win32 menu bar, the tray +//! menu, and native context menus. Each used to install its own handler or read +//! the receiver, so only one of them could ever work: whichever installed first +//! took the channel, a menu-bar click had nowhere to go because its items +//! carried no id at all, and a native context menu could not read its selection +//! once either of the others had installed a handler. +//! +//! This module owns the single handler. Consumers register a predicate and a +//! callback; each event is offered to the registered consumers in order and the +//! first predicate that claims the id handles it. A context menu that is +//! currently showing is offered every event first, because it is modal. + +use std::sync::{Mutex, OnceLock}; + +/// Claims an id and handles it. Returning `true` means this consumer took the +/// event and no later one should see it. +type Consumer = Box bool + Send + Sync>; + +/// Resolves the id a native context menu reported, while one is showing. +type ContextWaiter = Box; + +/// Who handled an event, for the tests and for a caller that wants to know. +#[derive(Debug, PartialEq, Eq)] +enum Handled { + /// A pending native context menu. + ContextMenu, + /// A registered consumer, by its position in the registration order. + Consumer(usize), + /// Nobody claimed the id; it was dropped. + Unclaimed, +} + +static CONSUMERS: Mutex> = Mutex::new(Vec::new()); +static CONTEXT_WAITER: Mutex> = Mutex::new(None); +static INSTALLED: OnceLock<()> = OnceLock::new(); + +/// Offers `id` to the consumers, in registration order, and returns who took it. +/// +/// This is the whole routing rule, and the installed handler is a thin wrapper +/// over it, so a test can exercise the real decision instead of a copy of it. +/// A context menu is checked first because it is modal: while one is showing it +/// is the only menu that can have produced an event. +fn route(id: &str) -> Handled { + if let Ok(mut slot) = CONTEXT_WAITER.lock() { + if let Some(callback) = slot.take() { + callback(id.to_string()); + return Handled::ContextMenu; + } + } + if let Ok(consumers) = CONSUMERS.lock() { + for (index, consumer) in consumers.iter().enumerate() { + if consumer(id) { + return Handled::Consumer(index); + } + } + } + Handled::Unclaimed +} + +/// Installs the single handler. Idempotent: the first call installs it and later +/// calls do nothing, which is what stops a second menu system from taking the +/// channel away from the first. +/// +/// Two crates deliver menu events, each with exactly one channel, and this app +/// builds menus with both: +/// +/// - `muda`, this crate's own dependency, builds the Win32 menu bar and the native +/// context menus on macOS and Windows. +/// - `tray_icon` bundles its own, newer `muda` and re-exports it as +/// `tray_icon::menu`, and the tray's menu is built with that copy on every +/// platform. The two are different versions — the lockfile holds both — so their +/// `MenuEvent`s are different channels even though the type has the same name, +/// and a selection from a tray menu arrives on `tray_icon`'s, never on `muda`'s. +/// Listening only on `muda`'s is what left a tray menu's items doing nothing on +/// macOS and Windows. +/// +/// Linux's GPUI-drawn menus (the menu bar and context menus) do not use an event +/// channel at all; they report through their own responder. +fn ensure_installed() { + INSTALLED.get_or_init(|| { + #[cfg(any(target_os = "macos", target_os = "windows"))] + { + let _ = muda::MenuEvent::set_event_handler(Some(|event: muda::MenuEvent| { + route(&event.id.0); + })); + } + let _ = tray_icon::menu::MenuEvent::set_event_handler(Some( + |event: tray_icon::menu::MenuEvent| { + route(&event.id.0); + }, + )); + }); +} + +/// Registers a consumer for menu ids it recognises. Registration order is the +/// order ids are offered in, so the first registration that claims an id wins. +pub fn register(consumer: Consumer) { + if let Ok(mut consumers) = CONSUMERS.lock() { + consumers.push(consumer); + } + ensure_installed(); +} + +/// Runs `show`, collecting the id the native context menu reports. +/// +/// The menu is modal and `show` returns when it closes, so the selection has +/// already arrived by the time this returns. Replacing a previous waiter is +/// deliberate: a stale one belongs to a menu that is no longer up. +pub fn with_context_menu(show: F) -> Option +where + F: FnOnce(), +{ + ensure_installed(); + let (sender, receiver) = std::sync::mpsc::channel::(); + if let Ok(mut slot) = CONTEXT_WAITER.lock() { + *slot = Some(Box::new(move |id| { + let _ = sender.send(id); + })); + } + show(); + // Clear the reservation whether or not anything was chosen, so a later + // application-menu event is not swallowed by a menu that has closed. + if let Ok(mut slot) = CONTEXT_WAITER.lock() { + *slot = None; + } + receiver.try_recv().ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Each test owns the routing state it asserts on. The statics are + /// process-wide, so tests that share them would interleave; this replaces + /// the consumer list and the waiter for the duration of one test. + struct Isolated { + _guard: std::sync::MutexGuard<'static, ()>, + } + + static TEST_LOCK: Mutex<()> = Mutex::new(()); + + impl Isolated { + fn new() -> Self { + let guard = TEST_LOCK.lock().unwrap_or_else(|error| error.into_inner()); + CONSUMERS.lock().unwrap().clear(); + *CONTEXT_WAITER.lock().unwrap() = None; + Self { _guard: guard } + } + } + + impl Drop for Isolated { + fn drop(&mut self) { + CONSUMERS.lock().unwrap().clear(); + *CONTEXT_WAITER.lock().unwrap() = None; + } + } + + #[test] + fn the_first_consumer_that_claims_an_id_handles_it() { + let _isolated = Isolated::new(); + register(Box::new(|id| id == "second-claims-this")); + register(Box::new(|id| id == "second-claims-this")); + register(Box::new(|_| false)); + // The first registration that claims it wins, so the second is never + // consulted for this id. + assert_eq!(route("second-claims-this"), Handled::Consumer(0)); + } + + #[test] + fn a_consumer_that_does_not_claim_an_id_is_passed_over() { + let _isolated = Isolated::new(); + register(Box::new(|id| id == "not-this-one")); + register(Box::new(|id| id == "mine")); + assert_eq!(route("mine"), Handled::Consumer(1)); + } + + #[test] + fn an_unclaimed_id_is_dropped_rather_than_misrouted() { + let _isolated = Isolated::new(); + register(Box::new(|id| id == "mine")); + // An unrelated menu's id must not be handed to this consumer: the two + // emit different JS events, so a misroute is a wrong action. + assert_eq!(route("someone-elses"), Handled::Unclaimed); + } + + #[test] + fn a_pending_context_menu_takes_every_event_and_then_stops() { + let _isolated = Isolated::new(); + register(Box::new(|_| true)); + // Reserve the channel the way `with_context_menu` does, then route an + // id: it goes to the context menu, not to the registered consumer. + let (sender, receiver) = std::sync::mpsc::channel::(); + *CONTEXT_WAITER.lock().unwrap() = Some(Box::new(move |id| { + let _ = sender.send(id); + })); + assert_eq!(route("cm-open"), Handled::ContextMenu); + assert_eq!(receiver.try_recv().ok().as_deref(), Some("cm-open")); + // The reservation is one-shot: the next event belongs to the normal + // route again, so a menu that has closed cannot swallow it. + assert_eq!(route("cm-open"), Handled::Consumer(0)); + } + + #[test] + fn with_context_menu_reports_the_selection_and_clears_itself() { + let _isolated = Isolated::new(); + register(Box::new(|_| true)); + // Simulate the menu closing with a choice: the callback is taken while + // `show` runs, exactly as `muda` does when the menu closes. + let selection = with_context_menu(|| { + if let Some(callback) = CONTEXT_WAITER.lock().unwrap().take() { + callback("cm-nested".to_string()); + } + }); + assert_eq!(selection.as_deref(), Some("cm-nested")); + assert!( + CONTEXT_WAITER.lock().unwrap().is_none(), + "the reservation must not outlive the menu" + ); + } + + #[test] + fn with_context_menu_reports_nothing_when_the_menu_is_dismissed() { + let _isolated = Isolated::new(); + let selection = with_context_menu(|| {}); + assert_eq!(selection, None); + assert!(CONTEXT_WAITER.lock().unwrap().is_none()); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/menu_model.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/menu_model.rs new file mode 100644 index 0000000000..eae1ad69db --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/menu_model.rs @@ -0,0 +1,592 @@ +use crate::desktop::errors::DesktopError; +use serde_json::Value; + +/// Nesting limit for submenus. Deeper than any real menu, shallow enough that a +/// malformed payload cannot recurse without bound. +pub const MAX_DEPTH: usize = 10; + +#[derive(Clone, Debug, PartialEq)] +pub enum MenuNode { + Item(MenuItemDef), + Submenu(SubmenuDef), + Separator, + StandardMenu(MenuRole), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct MenuItemDef { + pub id: String, + pub label: String, + pub accelerator: Option, + pub enabled: bool, + pub checked: bool, + pub role: Option, + /// The URI of the item's icon, loaded by `desktop::icon`. + pub icon: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SubmenuDef { + pub id: Option, + pub label: String, + pub enabled: bool, + pub items: Vec, + pub icon: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MenuRole { + About, + Services, + Hide, + HideOthers, + ShowAll, + Quit, + Undo, + Redo, + Cut, + Copy, + Paste, + SelectAll, + Minimize, + Zoom, + ToggleFullscreen, + Close, + Help, + AppMenu, + EditMenu, + WindowMenu, + HelpMenu, +} + +impl MenuRole { + pub fn parse(name: &str) -> Option { + Some(match name { + "about" => Self::About, + "services" => Self::Services, + "hide" => Self::Hide, + "hideOthers" => Self::HideOthers, + "showAll" => Self::ShowAll, + "quit" => Self::Quit, + "undo" => Self::Undo, + "redo" => Self::Redo, + "cut" => Self::Cut, + "copy" => Self::Copy, + "paste" => Self::Paste, + "selectAll" => Self::SelectAll, + "minimize" => Self::Minimize, + "zoom" => Self::Zoom, + "toggleFullscreen" => Self::ToggleFullscreen, + "close" => Self::Close, + "help" => Self::Help, + "appMenu" => Self::AppMenu, + "editMenu" => Self::EditMenu, + "windowMenu" => Self::WindowMenu, + "helpMenu" => Self::HelpMenu, + _ => return None, + }) + } + + pub fn is_standard_menu(self) -> bool { + matches!( + self, + Self::AppMenu | Self::EditMenu | Self::WindowMenu | Self::HelpMenu + ) + } + + pub fn default_label(self) -> &'static str { + match self { + Self::About => "About", + Self::Services => "Services", + Self::Hide => "Hide", + Self::HideOthers => "Hide Others", + Self::ShowAll => "Show All", + Self::Quit => "Quit", + Self::Undo => "Undo", + Self::Redo => "Redo", + Self::Cut => "Cut", + Self::Copy => "Copy", + Self::Paste => "Paste", + Self::SelectAll => "Select All", + Self::Minimize => "Minimize", + Self::Zoom => "Zoom", + Self::ToggleFullscreen => "Toggle Full Screen", + Self::Close => "Close", + Self::Help => "Help", + Self::AppMenu => "App", + Self::EditMenu => "Edit", + Self::WindowMenu => "Window", + Self::HelpMenu => "Help", + } + } + + /// Whether this platform can implement the role. Roles with no meaning are + /// dropped from the built menu rather than shown disabled. + pub fn is_supported(self) -> bool { + match self { + Self::Services | Self::Hide | Self::HideOthers | Self::ShowAll => { + cfg!(target_os = "macos") + } + Self::About => cfg!(target_os = "macos"), + _ => true, + } + } +} + +#[derive(Clone, Debug, PartialEq, Default)] +pub struct MenuModel { + pub items: Vec, +} + +impl MenuModel { + pub fn ids(&self) -> Vec { + let mut ids = Vec::new(); + collect_ids(&self.items, &mut ids); + ids + } + + pub fn find(&self, id: &str) -> Option<&MenuItemDef> { + fn walk<'a>(items: &'a [MenuNode], id: &str) -> Option<&'a MenuItemDef> { + for node in items { + match node { + MenuNode::Item(item) if item.id == id => return Some(item), + MenuNode::Submenu(submenu) => { + if let Some(found) = walk(&submenu.items, id) { + return Some(found); + } + } + _ => {} + } + } + None + } + walk(&self.items, id) + } + + pub fn accelerators(&self) -> Vec<(String, String)> { + let mut bindings = Vec::new(); + collect_accelerators(&self.items, &mut bindings); + bindings + } + + pub fn has_help_menu(&self) -> bool { + self.items.iter().any(|node| match node { + MenuNode::StandardMenu(MenuRole::HelpMenu) => true, + MenuNode::Submenu(submenu) => submenu.label.eq_ignore_ascii_case("help"), + _ => false, + }) + } +} + +fn collect_ids(items: &[MenuNode], ids: &mut Vec) { + for node in items { + match node { + MenuNode::Item(item) => ids.push(item.id.clone()), + MenuNode::Submenu(submenu) => { + if let Some(id) = &submenu.id { + ids.push(id.clone()); + } + collect_ids(&submenu.items, ids); + } + _ => {} + } + } +} + +fn collect_accelerators(items: &[MenuNode], out: &mut Vec<(String, String)>) { + for node in items { + match node { + MenuNode::Item(item) => { + if let Some(accelerator) = &item.accelerator { + out.push((item.id.clone(), accelerator.clone())); + } + } + MenuNode::Submenu(submenu) => collect_accelerators(&submenu.items, out), + _ => {} + } + } +} + +pub fn parse_menu(value: &Value) -> Result { + let array = value + .as_array() + .ok_or_else(|| DesktopError::invalid_argument("a menu must be an array"))?; + let items = parse_items(array, 0)?; + let model = MenuModel { items }; + let mut seen = std::collections::HashSet::new(); + for id in model.ids() { + if !seen.insert(id.clone()) { + return Err(DesktopError::invalid_argument(format!( + "duplicate menu item id: {id}" + ))); + } + } + Ok(model) +} + +fn parse_items(array: &[Value], depth: usize) -> Result, DesktopError> { + if depth > MAX_DEPTH { + return Err(DesktopError::invalid_argument(format!( + "menus nest deeper than {MAX_DEPTH} levels" + ))); + } + let mut nodes = Vec::with_capacity(array.len()); + for entry in array { + nodes.push(parse_node(entry, depth)?); + } + Ok(nodes) +} + +fn parse_node(value: &Value, depth: usize) -> Result { + let object = value + .as_object() + .ok_or_else(|| DesktopError::invalid_argument("a menu item must be an object"))?; + let kind = object + .get("type") + .map(|value| { + value + .as_str() + .ok_or_else(|| DesktopError::invalid_argument("menu item `type` must be a string")) + }) + .transpose()? + .unwrap_or("item"); + + match kind { + "separator" => { + check_fields(object, &["type"])?; + Ok(MenuNode::Separator) + } + "submenu" => { + check_fields(object, &["type", "id", "label", "enabled", "items", "icon"])?; + let label = required_string(object, "label")?; + let enabled = optional_bool(object, "enabled")?.unwrap_or(true); + let items = object + .get("items") + .and_then(Value::as_array) + .ok_or_else(|| { + DesktopError::invalid_argument("a submenu requires an `items` array") + })?; + Ok(MenuNode::Submenu(SubmenuDef { + id: optional_string(object, "id")?, + label, + enabled, + items: parse_items(items, depth + 1)?, + icon: optional_string(object, "icon")?, + })) + } + "role" => { + check_fields(object, &["type", "id", "role", "label", "enabled"])?; + let role_name = required_string(object, "role")?; + let role = MenuRole::parse(&role_name).ok_or_else(|| { + DesktopError::invalid_argument(format!("unknown menu role: {role_name}")) + })?; + if role.is_standard_menu() { + return Ok(MenuNode::StandardMenu(role)); + } + Ok(MenuNode::Item(MenuItemDef { + id: optional_string(object, "id")?.unwrap_or_else(|| format!("role:{role_name}")), + label: optional_string(object, "label")? + .unwrap_or_else(|| role.default_label().to_string()), + accelerator: None, + enabled: optional_bool(object, "enabled")?.unwrap_or(true), + checked: false, + role: Some(role), + icon: None, + })) + } + "item" => { + check_fields( + object, + &[ + "type", + "id", + "label", + "accelerator", + "enabled", + "checked", + "role", + "icon", + ], + )?; + let role = match optional_string(object, "role")? { + Some(name) => Some(MenuRole::parse(&name).ok_or_else(|| { + DesktopError::invalid_argument(format!("unknown menu role: {name}")) + })?), + None => None, + }; + let label = match optional_string(object, "label")? { + Some(label) => label, + None => role + .map(|role| role.default_label().to_string()) + .ok_or_else(|| { + DesktopError::invalid_argument("a menu item requires a `label`") + })?, + }; + let id = match optional_string(object, "id")? { + Some(id) => id, + None if role.is_some() => format!("role:{}", label), + None => { + return Err(DesktopError::invalid_argument( + "a menu item requires an `id`", + )) + } + }; + let accelerator = match optional_string(object, "accelerator")? { + Some(spec) => Some(parse_accelerator(&spec)?), + None => None, + }; + Ok(MenuNode::Item(MenuItemDef { + id, + label, + accelerator, + enabled: optional_bool(object, "enabled")?.unwrap_or(true), + checked: optional_bool(object, "checked")?.unwrap_or(false), + role, + icon: optional_string(object, "icon")?, + })) + } + other => Err(DesktopError::invalid_argument(format!( + "unknown menu item type: {other}" + ))), + } +} + +fn check_fields( + object: &serde_json::Map, + allowed: &[&str], +) -> Result<(), DesktopError> { + for key in object.keys() { + if !allowed.contains(&key.as_str()) { + return Err(DesktopError::invalid_argument(format!( + "unknown menu item field: {key}" + ))); + } + } + Ok(()) +} + +fn required_string( + object: &serde_json::Map, + key: &str, +) -> Result { + optional_string(object, key)? + .ok_or_else(|| DesktopError::invalid_argument(format!("missing `{key}`"))) +} + +fn optional_string( + object: &serde_json::Map, + key: &str, +) -> Result, DesktopError> { + match object.get(key) { + None | Some(Value::Null) => Ok(None), + Some(Value::String(value)) => Ok(Some(value.clone())), + Some(_) => Err(DesktopError::invalid_argument(format!( + "`{key}` must be a string" + ))), + } +} + +fn optional_bool( + object: &serde_json::Map, + key: &str, +) -> Result, DesktopError> { + match object.get(key) { + None | Some(Value::Null) => Ok(None), + Some(Value::Bool(value)) => Ok(Some(*value)), + Some(_) => Err(DesktopError::invalid_argument(format!( + "`{key}` must be a boolean" + ))), + } +} + +/// `CmdOrCtrl` becomes GPUI's `secondary`: Command on macOS, Control elsewhere. +pub fn parse_accelerator(spec: &str) -> Result { + let trimmed = spec.trim(); + if trimmed.is_empty() { + return Err(DesktopError::invalid_argument( + "an accelerator cannot be empty", + )); + } + let mut parts: Vec<&str> = trimmed.split('+').map(str::trim).collect(); + let key_token = parts + .pop() + .filter(|key| !key.is_empty()) + .ok_or_else(|| DesktopError::invalid_argument(format!("invalid accelerator: {spec}")))?; + + let mut modifiers: Vec<&'static str> = Vec::new(); + for part in parts { + if part.is_empty() { + return Err(DesktopError::invalid_argument(format!( + "invalid accelerator: {spec}" + ))); + } + let modifier = match part.to_ascii_lowercase().as_str() { + "cmdorctrl" | "commandorcontrol" | "cmdorcontrol" | "controllorcmd" => "secondary", + "command" | "cmd" | "super" | "meta" => "cmd", + "control" | "ctrl" => "ctrl", + "alt" | "option" => "alt", + "shift" => "shift", + "altgr" => { + return Err(DesktopError::invalid_argument( + "AltGr is not supported in accelerators", + )) + } + other => { + return Err(DesktopError::invalid_argument(format!( + "unknown accelerator modifier: {other}" + ))) + } + }; + if !modifiers.contains(&modifier) { + modifiers.push(modifier); + } + } + + let key = accelerator_key(key_token).ok_or_else(|| { + DesktopError::invalid_argument(format!("unknown accelerator key: {key_token}")) + })?; + + let mut keystroke = String::new(); + for modifier in modifiers { + keystroke.push_str(modifier); + keystroke.push('-'); + } + keystroke.push_str(&key); + Ok(keystroke) +} + +fn accelerator_key(token: &str) -> Option { + let lower = token.to_ascii_lowercase(); + if token.chars().count() == 1 { + let character = token.chars().next()?; + if character.is_ascii_alphanumeric() { + return Some(lower); + } + return Some(character.to_string()); + } + if let Some(rest) = lower.strip_prefix('f') { + if let Ok(number) = rest.parse::() { + if (1..=24).contains(&number) { + return Some(format!("f{number}")); + } + } + } + Some( + match lower.as_str() { + "enter" | "return" => "enter", + "escape" | "esc" => "escape", + "tab" => "tab", + "space" | "spacebar" => "space", + "backspace" => "backspace", + "delete" | "del" => "delete", + "insert" | "ins" => "insert", + "up" | "arrowup" => "up", + "down" | "arrowdown" => "down", + "left" | "arrowleft" => "left", + "right" | "arrowright" => "right", + "home" => "home", + "end" => "end", + "pageup" => "pageup", + "pagedown" => "pagedown", + "plus" => "+", + "minus" => "-", + "comma" => ",", + "period" => ".", + "slash" => "/", + "backslash" => "\\", + "semicolon" => ";", + "quote" => "'", + "backquote" => "`", + "bracketleft" => "[", + "bracketright" => "]", + _ => return None, + } + .to_string(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn parses_a_nested_menu() { + let model = parse_menu(&json!([ + {"label": "File", "type": "submenu", "items": [ + {"id": "new", "label": "New", "accelerator": "CmdOrCtrl+N"}, + {"type": "separator"}, + {"id": "quit", "label": "Quit", "role": "quit"} + ]}, + {"type": "role", "role": "editMenu"} + ])) + .expect("valid menu"); + assert_eq!(model.items.len(), 2); + assert_eq!(model.ids(), vec!["new".to_string(), "quit".to_string()]); + assert!(model.find("new").is_some()); + assert!(matches!( + model.items[1], + MenuNode::StandardMenu(MenuRole::EditMenu) + )); + } + + #[test] + fn rejects_duplicate_ids() { + let error = parse_menu(&json!([ + {"id": "a", "label": "A"}, + {"id": "a", "label": "Again"} + ])) + .expect_err("duplicate ids are rejected"); + assert_eq!(error.code, crate::desktop::errors::codes::INVALID_ARGUMENT); + assert!(error.message.contains("duplicate")); + } + + #[test] + fn rejects_unknown_fields() { + let error = parse_menu(&json!([{"id": "a", "label": "A", "typo": true}])) + .expect_err("unknown fields are rejected"); + assert!(error.message.contains("typo")); + } + + #[test] + fn rejects_unknown_types_and_roles() { + assert!(parse_menu(&json!([{"type": "magic"}])).is_err()); + assert!(parse_menu(&json!([{"type": "role", "role": "nope"}])).is_err()); + } + + #[test] + fn rejects_too_deep_nesting() { + let mut value = json!({"id": "leaf", "label": "leaf"}); + for _ in 0..MAX_DEPTH + 2 { + value = json!({"label": "sub", "type": "submenu", "items": [value]}); + } + assert!(parse_menu(&json!([value])).is_err()); + } + + #[test] + fn parses_accelerators() { + assert_eq!(parse_accelerator("CmdOrCtrl+N").unwrap(), "secondary-n"); + assert_eq!( + parse_accelerator("CmdOrCtrl+Shift+K").unwrap(), + "secondary-shift-k" + ); + assert_eq!(parse_accelerator("Ctrl+Alt+I").unwrap(), "ctrl-alt-i"); + assert_eq!(parse_accelerator("F5").unwrap(), "f5"); + assert_eq!(parse_accelerator("Cmd+,").unwrap(), "cmd-,"); + assert_eq!(parse_accelerator("Shift+Enter").unwrap(), "shift-enter"); + } + + #[test] + fn rejects_bad_accelerators() { + assert!(parse_accelerator("").is_err()); + assert!(parse_accelerator("Meta+UnknownKey").is_err()); + assert!(parse_accelerator("Hyper+K").is_err()); + } + + #[test] + fn role_menus_are_standard_menus() { + assert!(MenuRole::EditMenu.is_standard_menu()); + assert!(!MenuRole::Copy.is_standard_menu()); + assert!(!MenuRole::Copy.default_label().is_empty()); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/mod.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/mod.rs new file mode 100644 index 0000000000..ee0f8e1396 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/mod.rs @@ -0,0 +1,71 @@ +pub mod app_menu; +pub mod capabilities; +pub mod client_frame; +pub mod context_menu; +pub mod dev_menu; +pub mod errors; +pub mod gpui_menu; +pub mod icon; +pub mod keymap; +pub mod menu_events; +pub mod menu_model; +pub mod module; +pub mod perf_monitor; +pub mod platform_window; +pub mod tray; +pub mod windows; + +use gpui::App; +use std::sync::OnceLock; + +#[derive(Clone)] +pub struct Snapshot { + pub app_id: String, + pub platform: &'static str, +} + +static SNAPSHOT: OnceLock = OnceLock::new(); + +/// Install the snapshot the module's value methods read. Must run before +/// `load_script`, so a module that reads a capability during initialization sees +/// the real value. +pub fn install_snapshot(app_id: &str) { + let _ = SNAPSHOT.set(Snapshot { + app_id: app_id.to_string(), + platform: std::env::consts::OS, + }); +} + +/// The app id, or an empty string before `install_snapshot` ran. +pub fn snapshot_app_id() -> String { + SNAPSHOT + .get() + .map(|snapshot| snapshot.app_id.clone()) + .unwrap_or_default() +} + +pub fn snapshot_platform() -> String { + SNAPSHOT + .get() + .map(|snapshot| snapshot.platform.to_string()) + .unwrap_or_else(|| std::env::consts::OS.to_string()) +} + +/// The main loop's queue, installed by the bootstrap before the first command. +static COMMAND_SENDER: OnceLock> = + OnceLock::new(); + +pub(crate) fn set_command_sender(sender: async_channel::Sender) { + let _ = COMMAND_SENDER.set(sender); +} + +/// Queues one command for the GPUI main thread. The command answers through the +/// channel it was built with, which may be long after it returns: a context menu +/// answers when it closes. +pub fn post_to_main(command: Box) { + let Some(sender) = COMMAND_SENDER.get() else { + log::warn!("[desktop] the main loop is not running; command dropped"); + return; + }; + let _ = sender.send_blocking(crate::bootstrap::MainEvent::DesktopCommand(command)); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/module.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/module.rs new file mode 100644 index 0000000000..b0c2a67cff --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/module.rs @@ -0,0 +1,63 @@ +//! The desktop-shell TurboModule: one method per command in +//! `src-gpui/src/private/specs/NativeGpuiDesktop.ts`. +//! +//! A command needs the GPUI app context, which only the main loop holds, so +//! each one is posted to the main thread and awaited here. The value methods +//! (`capabilities`, `platform`, `appId`) read immutable state and run inline on +//! the JS thread, which is what makes them synchronous. + +use rn_gpui_module::{ModuleError, ModuleThread}; + +use super::capabilities; + +pub struct GpuiDesktopModule; + +rn_gpui_module::include_specs!(); + +impl NativeGpuiDesktopSpec for GpuiDesktopModule { + /// Every command runs where the GPUI app context is. + const THREAD: ModuleThread = ModuleThread::Main; + + fn new(_events: NativeGpuiDesktopEvents) -> Self { + Self + } + + fn capabilities(&self) -> Result { + Ok(DesktopCapabilities { + native_app_menu: capabilities::native_app_menu(), + native_context_menu: capabilities::native_context_menu(), + tray: capabilities::tray(), + tray_popover_anchored: capabilities::tray_popover_anchored(), + window_position: capabilities::window_position(), + always_on_top: capabilities::always_on_top(), + transparent_titlebar: capabilities::transparent_titlebar(), + menu_bar_in_window: capabilities::menu_bar_in_window(), + client_decorations: capabilities::client_decorations(), + }) + } + + fn platform(&self) -> Result { + Ok(super::snapshot_platform()) + } + + fn app_id(&self) -> Result { + Ok(super::snapshot_app_id()) + } + + async fn app_info(&self) -> Result { + Ok(DesktopAppInfo { + platform: super::snapshot_platform(), + arch: std::env::consts::ARCH.to_string(), + capabilities: self.capabilities()?, + }) + } + + fn add_listener(&self, _event_name: String) {} + + fn remove_listeners(&self, _count: f64) {} +} + +/// Registers the desktop module. Called once, before the first JS runtime. +pub fn register() { + rn_gpui_module::register(native_gpui_desktop_module::()); +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/perf_monitor.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/perf_monitor.rs new file mode 100644 index 0000000000..a137a19c37 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/perf_monitor.rs @@ -0,0 +1,413 @@ +//! A host-drawn GPUI overlay, not React Native views, so it does not disturb +//! what it measures. + +use crate::store::{store_from, SurfaceRootView}; +use gpui::{ + px, App, AppContext, Context, Entity, Global, IntoElement, ParentElement, Render, Styled, + Window, +}; +use serde_json::{json, Value}; +use std::cell::RefCell; +use std::collections::VecDeque; +use std::time::{Duration, Instant}; + +const WINDOW: Duration = Duration::from_secs(1); +const TEXT_REFRESH: Duration = Duration::from_millis(500); + +#[derive(Default)] +pub struct PerfMonitorState { + pub enabled: bool, + ui_frames: VecDeque, + js_ticks: VecDeque, + pub last_mount_ms: f32, + pub js_fps: f32, + js_probes_seen: u64, + js_probes_primed: bool, + memory: Option<(Instant, u64)>, + last_frame: Option, + /// The best rate seen since the monitor was enabled: the display's rate, + /// which is what a frame is late against. + peak_fps: f32, + dropped_frames: u64, + stutters: u64, +} + +const MIN_PEAK_FPS: f32 = 30.0; +/// React Native counts a stutter when at least this many frames drop at once. +const STUTTER_FRAMES: u64 = 4; + +fn missed_frames(gap: Duration, display_fps: f32) -> u64 { + if display_fps < MIN_PEAK_FPS { + return 0; + } + let refreshes = (gap.as_secs_f32() * display_fps).round(); + (refreshes - 1.0).max(0.0) as u64 +} + +pub struct PerfMonitorGlobal(pub RefCell); +impl Global for PerfMonitorGlobal {} + +impl PerfMonitorState { + fn prune(&mut self, now: Instant) { + while self + .ui_frames + .front() + .is_some_and(|at| now.duration_since(*at) > WINDOW) + { + self.ui_frames.pop_front(); + } + while self + .js_ticks + .front() + .is_some_and(|at| now.duration_since(*at) > WINDOW) + { + self.js_ticks.pop_front(); + } + } +} + +pub fn install(cx: &mut App) { + cx.set_global(PerfMonitorGlobal(RefCell::new(PerfMonitorState::default()))); +} + +pub fn set_enabled(cx: &mut App, enabled: bool) -> Value { + let was_enabled = { + let mut state = cx.global_mut::().0.borrow_mut(); + let previous = state.enabled; + state.enabled = enabled; + if !enabled { + state.ui_frames.clear(); + state.js_ticks.clear(); + state.js_fps = 0.0; + state.js_probes_primed = false; + state.last_frame = None; + state.dropped_frames = 0; + state.stutters = 0; + } + previous + }; + if enabled && !was_enabled { + cx.spawn(async move |cx| loop { + cx.background_executor().timer(TEXT_REFRESH).await; + let mut stop = false; + let _ = cx.update(|cx| { + if !is_enabled(cx) { + stop = true; + return; + } + let Some(store) = store_from(cx) else { + stop = true; + return; + }; + let monitor = store + .read(cx) + .roots + .get(&crate::store::APP_SURFACE_ID) + .and_then(|root| root.read(cx).perf_monitor.clone()); + if let Some(monitor) = monitor { + monitor.update(cx, |_monitor, cx| cx.notify()); + } + }); + if stop { + break; + } + }) + .detach(); + } + emit_enabled(cx, enabled); + let state = cx.global::().0.borrow(); + json!({"enabled": state.enabled}) +} + +fn emit_enabled(cx: &App, enabled: bool) { + let Some(store) = store_from(cx) else { + return; + }; + store.read(cx).host.emit_device_event_json( + "gpuiDesktop:perfMonitor", + &json!({"enabled": enabled}).to_string(), + ); +} + +pub fn is_enabled(cx: &App) -> bool { + cx.try_global::() + .is_some_and(|state| state.0.borrow().enabled) +} + +/// A presented frame. The overlay calls this once each time it is drawn, and +/// it is drawn once per frame while it is visible, so nothing else may call it: +/// a second caller would double the rate. +fn record_frame(cx: &App) { + let Some(global) = cx.try_global::() else { + return; + }; + let js_probes = store_from(cx).map(|store| store.read(cx).host.js_frame_probe()); + let mut state = global.0.borrow_mut(); + if !state.enabled { + return; + } + let now = Instant::now(); + state.prune(now); + state.ui_frames.push_back(now); + state.peak_fps = state.peak_fps.max(state.ui_frames.len() as f32); + if let Some(previous) = state.last_frame.replace(now) { + let missed = missed_frames(now.duration_since(previous), state.peak_fps); + state.dropped_frames += missed; + if missed >= STUTTER_FRAMES { + state.stutters += 1; + } + } + // One probe is posted to the JS thread per frame, and none while the + // previous one waits. Counting the ones that ran gives the rate the JS + // thread keeps up with: the frame rate when idle, less when it is busy. + if let Some(total) = js_probes { + let ran = total.saturating_sub(state.js_probes_seen); + state.js_probes_seen = total; + if state.js_probes_primed { + for _ in 0..ran.min(1000) { + state.js_ticks.push_back(now); + } + } + state.js_probes_primed = true; + } + state.js_fps = state.js_ticks.len() as f32; +} + +pub fn record_js_ticks(_cx: &App, _count: u32) {} + +pub fn record_mount(cx: &App, millis: f32) { + let Some(global) = cx.try_global::() else { + return; + }; + let mut state = global.0.borrow_mut(); + if state.enabled { + state.last_mount_ms = millis; + } +} + +pub fn metrics(cx: &App) -> Value { + let now = Instant::now(); + let (ui_fps, js_fps, mount_ms, enabled, dropped, stutters) = { + let Some(global) = cx.try_global::() else { + // The spec's metrics carry every field, so an app that never enabled + // the monitor reads zeros rather than a missing field. + return json!({ + "enabled": false, + "uiFps": 0.0, + "jsFps": 0.0, + "memoryBytes": 0, + "jsHeapBytes": 0, + "droppedFrames": 0, + "stutters": 0, + "mountedViews": 0, + "lastMountMs": 0.0, + }); + }; + let mut state = global.0.borrow_mut(); + state.prune(now); + // A JS thread that stopped running probes has no ticks left once the + // window passes; the stored rate would otherwise freeze at its last value. + state.js_fps = state.js_ticks.len() as f32; + ( + state.ui_frames.len() as f32, + state.js_fps, + state.last_mount_ms, + state.enabled, + state.dropped_frames, + state.stutters, + ) + }; + let memory = { + let global = cx.global::(); + let mut state = global.0.borrow_mut(); + match state.memory { + Some((at, bytes)) if now.duration_since(at) < TEXT_REFRESH => bytes, + _ => { + let bytes = resident_memory_bytes(); + state.memory = Some((now, bytes)); + bytes + } + } + }; + let (views, js_heap) = store_from(cx) + .map(|store| { + let store = store.read(cx); + (store.nodes.len(), store.host.js_heap_bytes()) + }) + .unwrap_or((0, 0)); + json!({ + "enabled": enabled, + "uiFps": ui_fps, + "jsFps": js_fps, + "memoryBytes": memory, + "jsHeapBytes": js_heap, + "droppedFrames": dropped, + "stutters": stutters, + "mountedViews": views, + "lastMountMs": mount_ms, + }) +} + +#[cfg(target_os = "macos")] +pub fn resident_memory_bytes() -> u64 { + // `task_info(TASK_BASIC_INFO)` reports `resident_size` in bytes. + #[repr(C)] + #[derive(Default)] + struct TaskBasicInfo { + virtual_size: u64, + resident_size: u64, + resident_size_max: u64, + user_time: [u32; 2], + system_time: [u32; 2], + policy: i32, + suspend_count: i32, + } + const TASK_BASIC_INFO: u32 = 4; + extern "C" { + fn mach_task_self() -> u32; + fn task_info(task: u32, flavor: u32, info: *mut TaskBasicInfo, count: *mut u32) -> i32; + } + let mut info = TaskBasicInfo::default(); + let mut count = (std::mem::size_of::() / std::mem::size_of::()) as u32; + // SAFETY: `info` and `count` are valid for `task_info` to write, and `count` is the buffer size in `u32`s. + let result = unsafe { task_info(mach_task_self(), TASK_BASIC_INFO, &mut info, &mut count) }; + if result == 0 { + info.resident_size + } else { + 0 + } +} + +#[cfg(target_os = "linux")] +pub fn resident_memory_bytes() -> u64 { + // `/proc/self/statm` field 2 is resident pages. + std::fs::read_to_string("/proc/self/statm") + .ok() + .and_then(|contents| { + contents + .split_whitespace() + .nth(1) + .and_then(|pages| pages.parse::().ok()) + }) + .map(|pages| pages * 4096) + .unwrap_or(0) +} + +#[cfg(not(any(target_os = "macos", target_os = "linux")))] +pub fn resident_memory_bytes() -> u64 { + // TODO: read `GetProcessMemoryInfo` on Windows. + 0 +} + +pub struct PerfMonitor { + window_id: u32, +} + +impl PerfMonitor { + pub fn new(window_id: u32) -> Self { + Self { window_id } + } +} + +fn format_bytes(bytes: u64) -> String { + const MB: f64 = 1024.0 * 1024.0; + if bytes == 0 { + "n/a".to_string() + } else { + format!("{:.0} MB", bytes as f64 / MB) + } +} + +impl Render for PerfMonitor { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if !is_enabled(cx) { + // Kept absolute so the hidden overlay is exactly 0x0 and cannot + // swallow input meant for the app behind it. + return gpui::div().absolute().top_0().left_0(); + } + // This runs once per presented frame while the overlay is visible. Asking + // for the next frame here is what keeps them coming, and it is legal + // from the paint phase (unlike from a timer). + record_frame(cx); + window.request_animation_frame(); + let value = metrics(cx); + let line = |label: &str, text: String| { + gpui::div() + .flex() + .gap_2() + .child(gpui::div().w(px(74.0)).child(label.to_string())) + .child(text) + }; + gpui::div() + .absolute() + .top(px(6.0)) + .right(px(6.0)) + .px_2() + .py_1() + .rounded(px(4.0)) + .bg(gpui::rgba(0x00000099)) + .text_color(gpui::white()) + .text_size(px(11.0)) + .flex() + .flex_col() + .gap_0p5() + .child(line( + "UI FPS", + format!("{:.0}", value["uiFps"].as_f64().unwrap_or(0.0)), + )) + .child(line( + "JS FPS", + format!("{:.0}", value["jsFps"].as_f64().unwrap_or(0.0)), + )) + .child(line( + "Dropped", + format!( + "{} ({} stutters)", + value["droppedFrames"].as_u64().unwrap_or(0), + value["stutters"].as_u64().unwrap_or(0) + ), + )) + .child(line("Views", format!("{}", value["mountedViews"]))) + .child(line( + "Mount", + format!("{:.1} ms", value["lastMountMs"].as_f64().unwrap_or(0.0)), + )) + .child(line( + "Memory", + format_bytes(value["memoryBytes"].as_u64().unwrap_or(0)), + )) + .child(line( + "JS heap", + format_bytes(value["jsHeapBytes"].as_u64().unwrap_or(0)), + )) + .child(line("Window", format!("#{}", self.window_id))) + } +} + +pub fn attach(cx: &mut App, root: &Entity, window_id: u32) { + let monitor = cx.new(|_| PerfMonitor::new(window_id)); + root.update(cx, |root, cx| { + root.perf_monitor = Some(monitor); + cx.notify(); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn missed_frames_counts_refreshes_without_a_frame() { + let frame = Duration::from_secs_f32(1.0 / 120.0); + assert_eq!(missed_frames(frame, 120.0), 0); + assert_eq!(missed_frames(frame * 2, 120.0), 1); + assert_eq!(missed_frames(frame * 5, 120.0), 4); + assert_eq!(missed_frames(frame * 5, 0.0), 0); + } + + #[test] + fn format_bytes_reports_megabytes() { + assert_eq!(format_bytes(0), "n/a"); + assert_eq!(format_bytes(64 * 1024 * 1024), "64 MB"); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/platform_window.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/platform_window.rs new file mode 100644 index 0000000000..6a0a371974 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/platform_window.rs @@ -0,0 +1,1068 @@ +pub fn supports_position() -> bool { + crate::desktop::capabilities::window_position() +} + +#[cfg(target_os = "macos")] +mod imp { + use cocoa::base::id; + use cocoa::foundation::{NSPoint, NSSize}; + use objc::{msg_send, sel, sel_impl}; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + + const FLOATING_WINDOW_LEVEL: i64 = 3; + const NORMAL_WINDOW_LEVEL: i64 = 0; + + fn ns_window(window: &gpui::Window) -> Result { + let handle = HasWindowHandle::window_handle(window) + .map_err(|error| format!("no platform window handle: {error}"))?; + match handle.as_raw() { + // SAFETY: `ns_view` comes from the window's raw handle, which GPUI keeps valid while the window lives. + RawWindowHandle::AppKit(appkit) => unsafe { + let ns_view = appkit.ns_view.as_ptr() as id; + let native: id = msg_send![ns_view, window]; + if native.is_null() { + Err("the AppKit window is gone".to_string()) + } else { + Ok(native) + } + }, + _ => Err("unexpected platform window handle".to_string()), + } + } + + /// Convert a GPUI top-left-origin screen point into an AppKit top-left point. + /// + /// GPUI's `bounds()` reports `x - screen.origin.x` and + /// `screen.height - appkit_y - frame_height + screen.origin.y`; this is the + /// inverse, so `set_position` round-trips through `window.bounds()`. + fn appkit_top_left(window: &gpui::Window, native: id, x: f32, y: f32) -> NSPoint { + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let screen: id = msg_send![native, screen]; + let frame_height = f32::from(window.bounds().size.height); + if screen.is_null() { + return NSPoint::new(f64::from(x), f64::from(y)); + } + let screen_frame: cocoa::foundation::NSRect = msg_send![screen, frame]; + let appkit_x = x + screen_frame.origin.x as f32; + let appkit_y = screen_frame.size.height as f32 - (y - screen_frame.origin.y as f32); + NSPoint::new(f64::from(appkit_x), f64::from(appkit_y + frame_height)) + } + } + + pub fn set_position(window: &mut gpui::Window, x: f32, y: f32) -> Result<(), String> { + let native = ns_window(window)?; + let point = appkit_top_left(window, native, x, y); + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let _: () = msg_send![native, setFrameTopLeftPoint: point]; + } + Ok(()) + } + + pub fn set_always_on_top(window: &mut gpui::Window, on: bool) -> Result<(), String> { + let native = ns_window(window)?; + let level = if on { + FLOATING_WINDOW_LEVEL + } else { + NORMAL_WINDOW_LEVEL + }; + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let _: () = msg_send![native, setLevel: level]; + } + Ok(()) + } + + pub fn set_min_size(window: &mut gpui::Window, width: f32, height: f32) -> Result<(), String> { + let native = ns_window(window)?; + let size = NSSize::new(f64::from(width), f64::from(height)); + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let _: () = msg_send![native, setContentMinSize: size]; + } + Ok(()) + } + + pub fn set_max_size(window: &mut gpui::Window, width: f32, height: f32) -> Result<(), String> { + let native = ns_window(window)?; + let size = NSSize::new(f64::from(width), f64::from(height)); + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let _: () = msg_send![native, setContentMaxSize: size]; + } + Ok(()) + } + + const STYLE_MASK_RESIZABLE: u64 = 1 << 3; + + pub fn set_resizable(window: &mut gpui::Window, resizable: bool) -> Result<(), String> { + let native = ns_window(window)?; + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let mask: u64 = msg_send![native, styleMask]; + let next = if resizable { + mask | STYLE_MASK_RESIZABLE + } else { + mask & !STYLE_MASK_RESIZABLE + }; + if next != mask { + let _: () = msg_send![native, setStyleMask: next]; + } + } + Ok(()) + } + + pub fn show(window: &mut gpui::Window) -> Result<(), String> { + let native = ns_window(window)?; + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let _: () = msg_send![native, orderFront: std::ptr::null::()]; + } + Ok(()) + } + + pub fn hide(window: &mut gpui::Window) -> Result<(), String> { + let native = ns_window(window)?; + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let _: () = msg_send![native, orderOut: std::ptr::null::()]; + } + Ok(()) + } + + pub fn is_minimized(window: &gpui::Window) -> bool { + let Ok(native) = ns_window(window) else { + return false; + }; + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let minimized: bool = msg_send![native, isMiniaturized]; + minimized + } + } + + /// `[NSWindow isZoomed]`. + /// + /// GPUI's own `Window::is_maximized` reads `NSScreen.visibleFrame` and + /// panics when the window has no screen (a hidden or off-screen window), + /// which the test driver creates, so the desktop shell asks AppKit instead. + pub fn is_maximized(window: &gpui::Window) -> bool { + let Ok(native) = ns_window(window) else { + return false; + }; + // SAFETY: `native` is the live `NSWindow` from `ns_window`, and AppKit is only messaged from the main thread. + unsafe { + let screen: id = msg_send![native, screen]; + if screen.is_null() { + return false; + } + let zoomed: bool = msg_send![native, isZoomed]; + zoomed + } + } + + /// `style_as_popover` already configures the panel; nothing to set before + /// it is shown. + pub fn prepare_popover( + _window: &mut gpui::Window, + _x: f32, + _y: f32, + _width: f32, + _height: f32, + ) -> Result<(), String> { + Ok(()) + } + + pub fn activate_popover(_window: &gpui::Window) -> Result<(), String> { + Ok(()) + } +} + +#[cfg(target_os = "macos")] +pub use imp::*; + +/// Windows: `SetWindowPos` / `ShowWindow` / `IsIconic` / `IsZoomed` on the +/// HWND. +/// +/// TODO: never built or run on Windows. +#[cfg(target_os = "windows")] +mod imp { + use gpui::Window; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + use windows_sys::Win32::Foundation::HWND; + use windows_sys::Win32::UI::WindowsAndMessaging::{ + IsIconic, IsWindowVisible, IsZoomed, SetWindowPos, ShowWindowAsync, HWND_NOTOPMOST, + HWND_TOPMOST, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, + SW_HIDE, SW_SHOW, + }; + + const UNSUPPORTED_MIN_MAX: &str = + "a runtime minimum/maximum size needs a WM_GETMINMAXINFO handler in GPUI's window procedure"; + + fn hwnd(window: &Window) -> Result { + let handle = HasWindowHandle::window_handle(window) + .map_err(|error| format!("no platform window handle: {error}"))?; + match handle.as_raw() { + RawWindowHandle::Win32(win32) => Ok(win32.hwnd.get() as HWND), + _ => Err("unexpected platform window handle".to_string()), + } + } + + pub fn set_position(window: &mut Window, x: f32, y: f32) -> Result<(), String> { + let hwnd = hwnd(window)?; + // GPUI's bounds are physical pixels, which is what SetWindowPos takes. + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + let result = unsafe { + SetWindowPos( + hwnd, + std::ptr::null_mut(), + x.round() as i32, + y.round() as i32, + 0, + 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE, + ) + }; + if result == 0 { + Err("SetWindowPos failed".to_string()) + } else { + Ok(()) + } + } + + pub fn set_always_on_top(window: &mut Window, on: bool) -> Result<(), String> { + let hwnd = hwnd(window)?; + let insert_after = if on { HWND_TOPMOST } else { HWND_NOTOPMOST }; + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + let result = unsafe { + SetWindowPos( + hwnd, + insert_after, + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ) + }; + if result == 0 { + Err("SetWindowPos failed".to_string()) + } else { + Ok(()) + } + } + + pub fn set_min_size(_window: &mut Window, _width: f32, _height: f32) -> Result<(), String> { + Err(UNSUPPORTED_MIN_MAX.to_string()) + } + + pub fn set_max_size(_window: &mut Window, _width: f32, _height: f32) -> Result<(), String> { + Err(UNSUPPORTED_MIN_MAX.to_string()) + } + + /// Toggle the resize frame. GPUI applies the style flags it knows at + /// creation, so the change is pushed with `SWP_FRAMECHANGED`. + pub fn set_resizable(window: &mut Window, resizable: bool) -> Result<(), String> { + use windows_sys::Win32::UI::WindowsAndMessaging::{ + GetWindowLongPtrW, SetWindowLongPtrW, GWL_STYLE, WS_MAXIMIZEBOX, WS_THICKFRAME, + }; + let hwnd = hwnd(window)?; + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + unsafe { + let style = GetWindowLongPtrW(hwnd, GWL_STYLE) as u32; + let style = if resizable { + style | WS_THICKFRAME | WS_MAXIMIZEBOX + } else { + style & !(WS_THICKFRAME | WS_MAXIMIZEBOX) + }; + SetWindowLongPtrW(hwnd, GWL_STYLE, style as isize); + SetWindowPos( + hwnd, + std::ptr::null_mut(), + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED, + ); + } + Ok(()) + } + + /// `ShowWindowAsync` rather than `ShowWindow`: the synchronous call delivers + /// `WM_SHOWWINDOW`/`WM_SIZE`/`WM_MOVE` before it returns, and GPUI's window + /// procedure re-enters the window state that the caller is inside, which + /// logs "RefCell already borrowed" (and drops the notification). The async + /// form queues the same messages and returns, which is what GPUI's own + /// `minimize` and `zoom` use. + pub fn show(window: &mut Window) -> Result<(), String> { + let hwnd = hwnd(window)?; + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + unsafe { ShowWindowAsync(hwnd, SW_SHOW) }; + Ok(()) + } + + pub fn hide(window: &mut Window) -> Result<(), String> { + let hwnd = hwnd(window)?; + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + unsafe { ShowWindowAsync(hwnd, SW_HIDE) }; + Ok(()) + } + + pub fn is_minimized(window: &Window) -> bool { + match hwnd(window) { + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + Ok(hwnd) => unsafe { IsIconic(hwnd) != 0 }, + Err(_) => false, + } + } + + /// Whether the window is not being presented. `IsWindowVisible` is false + /// for a window created with `show: false` (the offscreen test windows) and + /// after `ShowWindow(SW_HIDE)`, and stays true for a window that is merely + /// covered, which is what the test driver wants to distinguish. + pub fn is_hidden(window: &Window) -> bool { + match hwnd(window) { + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + Ok(hwnd) => unsafe { IsWindowVisible(hwnd) == 0 }, + Err(_) => false, + } + } + + pub fn is_maximized(window: &Window) -> bool { + match hwnd(window) { + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + Ok(hwnd) => unsafe { IsZoomed(hwnd) != 0 }, + Err(_) => false, + } + } + + /// Sets the popover up while it is still hidden: out of the taskbar and the + /// Alt+Tab list, above the other topmost windows, placed under (or over) its + /// tray icon, and at its own size. + /// + /// `x`, `y`, `width` and `height` are in points; `SetWindowPos` takes physical + /// pixels, so they are scaled by the window's own scale factor. + pub fn prepare_popover( + window: &mut Window, + x: f32, + y: f32, + width: f32, + height: f32, + ) -> Result<(), String> { + use windows_sys::Win32::UI::WindowsAndMessaging::{ + GetWindowLongPtrW, SetWindowLongPtrW, GWL_EXSTYLE, WS_EX_APPWINDOW, WS_EX_TOOLWINDOW, + }; + let hwnd = hwnd(window)?; + let scale = window.scale_factor(); + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + let placed = unsafe { + // A tool window has no taskbar button, which is also what a popover + // that shows the app's icon there would otherwise leave behind. + let style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE) as u32; + SetWindowLongPtrW( + hwnd, + GWL_EXSTYLE, + ((style | WS_EX_TOOLWINDOW) & !WS_EX_APPWINDOW) as isize, + ); + // Topmost, because the windows a tray click leaves open are: the + // taskbar's hidden-icons flyout is a topmost window, and a normal + // window opened from an icon inside it would appear behind it. A + // topmost window shown later goes above the topmost ones already + // there. + SetWindowPos( + hwnd, + HWND_TOPMOST, + (x * scale).round() as i32, + (y * scale).round() as i32, + (width * scale).round() as i32, + (height * scale).round() as i32, + SWP_NOACTIVATE | SWP_FRAMECHANGED, + ) + }; + if placed == 0 { + Err("SetWindowPos failed".to_string()) + } else { + Ok(()) + } + } + + /// Shows the popover, which activates it, so it takes focus — and so loses it + /// when the user clicks elsewhere, which is what dismisses it. A window that + /// was created hidden stays hidden until this. + /// + /// `ShowWindowAsync`, like `show`: the synchronous calls (`ShowWindow`, + /// `SetForegroundWindow`) deliver the activation messages before they return, + /// into a window procedure that is already inside GPUI's window state, which + /// logs "RefCell already borrowed". + pub fn activate_popover(window: &Window) -> Result<(), String> { + let hwnd = hwnd(window)?; + // SAFETY: `hwnd` is the live window handle `hwnd()` just returned, used on the window's own thread. + unsafe { ShowWindowAsync(hwnd, SW_SHOW) }; + Ok(()) + } +} + +/// X11: the window calls GPUI does not expose, over a second X connection. +/// Wayland has no absolute positioning or raise, so a Wayland session reports +/// `unsupported`. +/// +/// TODO: never built or run on Linux. +#[cfg(target_os = "linux")] +mod imp { + use gpui::Window; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + use x11_dl::xlib; + + const UNSUPPORTED_WAYLAND: &str = + "Wayland has no absolute window positioning or raise; use X11 for these calls"; + + fn wayland() -> bool { + crate::desktop::capabilities::is_wayland() + } + + fn window_id(window: &Window) -> Result { + let handle = HasWindowHandle::window_handle(window) + .map_err(|error| format!("no platform window handle: {error}"))?; + match handle.as_raw() { + RawWindowHandle::Xlib(xlib_handle) => Ok(xlib_handle.window as u64), + RawWindowHandle::Xcb(xcb) => Ok(xcb.window.get() as u64), + _ => Err("the session is not X11".to_string()), + } + } + + /// Run `f` with a fresh X connection. GPUI owns its own connection and does + /// not expose it, and the X protocol allows a second client to move and + /// raise another client's window. + fn with_display(f: impl FnOnce(&xlib::Xlib, *mut xlib::Display) -> R) -> Result { + if wayland() { + return Err(UNSUPPORTED_WAYLAND.to_string()); + } + let xlib = xlib::Xlib::open().map_err(|error| format!("no X11 library: {error}"))?; + // SAFETY: the display from `XOpenDisplay` is null-checked before use and closed once, after `f` returns. + unsafe { + let display = (xlib.XOpenDisplay)(std::ptr::null()); + if display.is_null() { + return Err("could not open an X11 display".to_string()); + } + let result = f(&xlib, display); + (xlib.XCloseDisplay)(display); + Ok(result) + } + } + + /// Xlib's functions live on the loaded library, so every call goes through + /// the `xlib` instance `with_display` hands to the closure. + fn with_window( + window: &Window, + f: impl FnOnce(&xlib::Xlib, *mut xlib::Display, xlib::Window) -> R, + ) -> Result { + let id = window_id(window)?; + with_display(|xlib, display| f(xlib, display, id as xlib::Window)) + } + + pub fn set_position(window: &mut Window, x: f32, y: f32) -> Result<(), String> { + // SAFETY: `display` is an open connection and `id` a window id for the whole closure; `with_display` closes the connection afterwards. + with_window(window, |xlib, display, id| unsafe { + (xlib.XMoveWindow)(display, id, x.round() as i32, y.round() as i32); + (xlib.XFlush)(display); + }) + } + + /// Raising is the part of always-on-top X11 itself can do, so `on == true` + /// only raises the window once and `on == false` is a no-op. + /// + /// TODO: set the `_NET_WM_STATE_ABOVE` hint so the window stays above. + pub fn set_always_on_top(window: &mut Window, on: bool) -> Result<(), String> { + // SAFETY: `display` is an open connection and `id` a window id for the whole closure; `with_display` closes the connection afterwards. + with_window(window, |xlib, display, id| unsafe { + if on { + (xlib.XRaiseWindow)(display, id); + (xlib.XFlush)(display); + } + }) + } + + pub fn set_min_size(window: &mut Window, width: f32, height: f32) -> Result<(), String> { + // SAFETY: `display` is an open connection and `id` a window id for the whole closure; `with_display` closes the connection afterwards. + with_window(window, |xlib, display, id| unsafe { + let mut hints: xlib::XSizeHints = std::mem::zeroed(); + hints.flags = xlib::PMinSize; + hints.min_width = width.round() as i32; + hints.min_height = height.round() as i32; + (xlib.XSetWMNormalHints)(display, id, &mut hints); + (xlib.XFlush)(display); + }) + } + + pub fn set_max_size(window: &mut Window, width: f32, height: f32) -> Result<(), String> { + // SAFETY: `display` is an open connection and `id` a window id for the whole closure; `with_display` closes the connection afterwards. + with_window(window, |xlib, display, id| unsafe { + let mut hints: xlib::XSizeHints = std::mem::zeroed(); + hints.flags = xlib::PMaxSize; + hints.max_width = width.round() as i32; + hints.max_height = height.round() as i32; + (xlib.XSetWMNormalHints)(display, id, &mut hints); + (xlib.XFlush)(display); + }) + } + + pub fn set_resizable(_window: &mut Window, _resizable: bool) -> Result<(), String> { + // TODO: GPUI sets the size hints from `WindowOptions::is_resizable`; + // changing them at runtime needs the WM_NORMAL_HINTS pair above plus a + // `_MOTIF_WM_HINTS` update. + Err("changing resizability at runtime is not implemented on X11 yet".to_string()) + } + + pub fn show(window: &mut Window) -> Result<(), String> { + // SAFETY: `display` is an open connection and `id` a window id for the whole closure; `with_display` closes the connection afterwards. + with_window(window, |xlib, display, id| unsafe { + (xlib.XMapWindow)(display, id); + (xlib.XFlush)(display); + }) + } + + pub fn hide(window: &mut Window) -> Result<(), String> { + // SAFETY: `display` is an open connection and `id` a window id for the whole closure; `with_display` closes the connection afterwards. + with_window(window, |xlib, display, id| unsafe { + (xlib.XUnmapWindow)(display, id); + (xlib.XFlush)(display); + }) + } + + pub fn is_minimized(window: &Window) -> bool { + // `XGetWindowAttributes` reports `IsUnmapped` for a hidden window, but + // X11 has no "minimized" state of its own (the WM owns it). + // SAFETY: `display` is an open connection and `id` a window id for the whole closure; `with_display` closes the connection afterwards. + with_window(window, |xlib, display, id| unsafe { + let mut attributes: xlib::XWindowAttributes = std::mem::zeroed(); + if (xlib.XGetWindowAttributes)(display, id, &mut attributes) == 0 { + return false; + } + attributes.map_state == xlib::IsUnmapped + }) + .unwrap_or(false) + } + + pub fn is_maximized(_window: &Window) -> bool { + // Only the window manager knows; it would need a `_NET_WM_STATE` read. + false + } + + /// Turns a just-created, still-unmapped window into a tray popover. + /// + /// The tray popover cannot be `WindowKind::PopUp`: GPUI makes that + /// override-redirect, so the window manager never focuses it, its + /// `install_activation_observer` never sees a deactivation, and it cannot be + /// dismissed by clicking away. It is instead an ordinary undecorated window + /// that the window manager manages — the same shape Electron's Linux tray + /// menu uses (a frameless window with skip-taskbar and always-on-top). The + /// properties below are set on the still-unmapped window so nothing flashes + /// in the taskbar or appears decorated first. + /// + /// Call before the window is mapped (see `windows::open_window`, which opens + /// the popover with `show: false` and maps it afterwards); `_NET_WM_STATE` + /// and `_NET_WM_WINDOW_TYPE` are read when the window is mapped. + pub fn prepare_popover( + window: &mut Window, + x: f32, + y: f32, + width: f32, + height: f32, + ) -> Result<(), String> { + // SAFETY: `display` is an open connection and `id` a window id for the whole closure; `with_display` closes the connection afterwards. + with_window(window, |xlib, display, id| unsafe { + // A hidden window is placed by the user, not the window manager: + // USPosition with the exact origin, so a WM that honours hints maps + // it where the tray icon is instead of centring it. + let mut hints: xlib::XSizeHints = std::mem::zeroed(); + hints.flags = xlib::USPosition | xlib::PSize; + hints.x = x.round() as i32; + hints.y = y.round() as i32; + hints.width = width.round() as i32; + hints.height = height.round() as i32; + (xlib.XSetWMNormalHints)(display, id, &mut hints); + + // `_NET_WM_WINDOW_TYPE_UTILITY`: a small tool window, which window + // managers keep out of the taskbar and alt-tab. `DIALOG` would want + // a `WM_TRANSIENT_FOR` parent and a tray-only app has none. + set_atoms( + xlib, + display, + id, + "_NET_WM_WINDOW_TYPE", + &["_NET_WM_WINDOW_TYPE_UTILITY"], + ); + + // `_NET_WM_STATE`: never in the taskbar or pager, always above. + set_atoms( + xlib, + display, + id, + "_NET_WM_STATE", + &[ + "_NET_WM_STATE_SKIP_TASKBAR", + "_NET_WM_STATE_SKIP_PAGER", + "_NET_WM_STATE_ABOVE", + ], + ); + + // Decorations off. GPUI's `request_decorations(Client)` already sets + // the motif hint for a frameless window; this is idempotent and also + // covers a server-decorations fallback. The five words are + // flags=MWM_HINTS_DECORATIONS, functions=0, decorations=0, ... + let motif = (xlib.XInternAtom)(display, b"_MOTIF_WM_HINTS\0".as_ptr() as *const _, 0); + if motif != 0 { + let data: [u32; 5] = [1 << 1, 0, 0, 0, 0]; + (xlib.XChangeProperty)( + display, + id, + motif, + motif, + 32, + xlib::PropModeReplace, + data.as_ptr() as *const std::os::raw::c_uchar, + 5, + ); + } + + (xlib.XFlush)(display); + }) + } + + /// Sets an `ATOM[]` property to the interned atoms named in `values`. + /// + /// Replaces the property (`PropModeReplace`), which is what the window + /// manager reads once, when the window is mapped. + /// + /// # Safety + /// + /// `display` must be an open X connection and `window` a valid window id on it. + unsafe fn set_atoms( + xlib: &xlib::Xlib, + display: *mut xlib::Display, + window: xlib::Window, + property: &str, + values: &[&str], + ) { + let property_atom = + (xlib.XInternAtom)(display, format!("{property}\0").as_ptr() as *const _, 0); + if property_atom == 0 { + return; + } + let atoms: Vec = values + .iter() + .map(|value| (xlib.XInternAtom)(display, format!("{value}\0").as_ptr() as *const _, 0)) + .filter(|atom| *atom != 0) + .collect(); + if atoms.is_empty() { + return; + } + (xlib.XChangeProperty)( + display, + window, + property_atom, + xlib::XA_ATOM, + 32, + xlib::PropModeReplace, + atoms.as_ptr() as *const std::os::raw::c_uchar, + atoms.len() as i32, + ); + } + + /// Asks the window manager to make `window` the active window. + /// + /// `activate_window()` sends `_NET_ACTIVE_WINDOW` with source indication 1 + /// (a normal application request) and `CurrentTime`. Both are the parts a + /// focus stealing prevention check distrusts: the request carries no + /// timestamp from the event that caused it (a tray click arrives over D-Bus, + /// which has no X timestamp), so a strict window manager may map the popover + /// without focusing it — and without focus the popover can never see the + /// deactivation that dismisses it. Source indication 2 (a pager / taskbar) + /// is trusted to set focus, and a fresh server timestamp avoids the + /// "timestamp in the past" rejection. + pub fn activate_popover(window: &Window) -> Result<(), String> { + // SAFETY: `display` is an open connection and `id` a window id for the whole closure; `with_display` closes the connection afterwards. + with_window(window, |xlib, display, id| unsafe { + let root = (xlib.XDefaultRootWindow)(display); + let atom = (xlib.XInternAtom)(display, b"_NET_ACTIVE_WINDOW\0".as_ptr() as *const _, 0); + let mut event: xlib::XEvent = std::mem::zeroed(); + { + let message = &mut event.client_message; + message.type_ = xlib::ClientMessage; + message.display = display; + message.window = id; + message.message_type = atom; + message.format = 32; + // data = [source indication, timestamp, requestor, 0, 0] + message.data.set_long(0, 2); + message.data.set_long(1, xlib::CurrentTime as i64); + message.data.set_long(2, id as i64); + } + (xlib.XSendEvent)( + display, + root, + 0, + xlib::SubstructureRedirectMask | xlib::SubstructureNotifyMask, + &mut event, + ); + (xlib.XFlush)(display); + }) + } +} + +#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] +mod imp { + use gpui::Window; + + const UNSUPPORTED: &str = + "window positioning and raising are not implemented on this platform yet"; + + pub fn set_position(_window: &mut Window, _x: f32, _y: f32) -> Result<(), String> { + Err(UNSUPPORTED.to_string()) + } + pub fn set_always_on_top(_window: &mut Window, _on: bool) -> Result<(), String> { + Err(UNSUPPORTED.to_string()) + } + pub fn set_min_size(_window: &mut Window, _w: f32, _h: f32) -> Result<(), String> { + Err(UNSUPPORTED.to_string()) + } + pub fn set_max_size(_window: &mut Window, _w: f32, _h: f32) -> Result<(), String> { + Err(UNSUPPORTED.to_string()) + } + pub fn set_resizable(_window: &mut Window, _resizable: bool) -> Result<(), String> { + Err(UNSUPPORTED.to_string()) + } + pub fn show(_window: &mut Window) -> Result<(), String> { + Err(UNSUPPORTED.to_string()) + } + pub fn hide(_window: &mut Window) -> Result<(), String> { + Err(UNSUPPORTED.to_string()) + } + pub fn is_minimized(_window: &Window) -> bool { + false + } + pub fn is_maximized(window: &Window) -> bool { + window.is_maximized() + } + pub fn prepare_popover( + _window: &mut Window, + _x: f32, + _y: f32, + _width: f32, + _height: f32, + ) -> Result<(), String> { + Err(UNSUPPORTED.to_string()) + } + pub fn activate_popover(_window: &Window) -> Result<(), String> { + Ok(()) + } +} + +#[cfg(not(target_os = "macos"))] +pub use imp::*; + +#[cfg(target_os = "macos")] +pub fn set_accessory_app(accessory: bool) { + use objc::{class, msg_send, sel, sel_impl}; + const REGULAR: isize = 0; + const ACCESSORY: isize = 1; + // SAFETY: the shared `NSApplication` is messaged from the main thread. + unsafe { + let app: cocoa::base::id = msg_send![class!(NSApplication), sharedApplication]; + let policy = if accessory { ACCESSORY } else { REGULAR }; + let _: bool = msg_send![app, setActivationPolicy: policy]; + } +} + +#[cfg(not(target_os = "macos"))] +pub fn set_accessory_app(_accessory: bool) {} + +/// Gives an unbundled run (`cargo run`) a Dock icon. A `.app` takes its icon +/// from its bundle, so a bundled run is left alone. +#[cfg(target_os = "macos")] +pub fn set_dock_icon(path: &std::path::Path) { + use cocoa::base::{id, nil}; + use cocoa::foundation::NSString; + use objc::{class, msg_send, sel, sel_impl}; + let Some(path) = path.to_str() else { + return; + }; + // SAFETY: the shared `NSApplication` and `NSBundle` are messaged from the + // main thread; the image is retained by the application once set. + unsafe { + let bundle: id = msg_send![class!(NSBundle), mainBundle]; + let bundle_path: id = msg_send![bundle, bundlePath]; + let suffix = NSString::alloc(nil).init_str(".app"); + let bundled: bool = msg_send![bundle_path, hasSuffix: suffix]; + let _: () = msg_send![suffix, release]; + if bundled { + return; + } + let file = NSString::alloc(nil).init_str(path); + let image: id = msg_send![class!(NSImage), alloc]; + let image: id = msg_send![image, initWithContentsOfFile: file]; + let _: () = msg_send![file, release]; + if image.is_null() { + log::warn!("[desktop] could not load the Dock icon {path}"); + return; + } + let app: id = msg_send![class!(NSApplication), sharedApplication]; + let _: () = msg_send![app, setApplicationIconImage: image]; + let _: () = msg_send![image, release]; + } +} + +#[cfg(not(target_os = "macos"))] +pub fn set_dock_icon(_path: &std::path::Path) {} + +/// Gives a window its icon on Windows, which is what the title bar and the +/// taskbar show. The executable carries no icon of its own in a dev build, so +/// without this the window keeps the default one. +/// +/// `WM_SETICON` takes an `HICON`, and Windows (Vista and later) builds one from +/// PNG bytes directly, so the file is passed through unchanged. One icon is +/// made per size: the large one for the taskbar, the small one for the title +/// bar. +#[cfg(target_os = "windows")] +pub fn set_window_icon(window: &gpui::Window, path: &std::path::Path) { + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + use windows_sys::Win32::UI::WindowsAndMessaging::{ + CreateIconFromResourceEx, GetSystemMetrics, SendMessageW, ICON_BIG, ICON_SMALL, + LR_DEFAULTCOLOR, SM_CXICON, SM_CXSMICON, WM_SETICON, + }; + + let Ok(bytes) = std::fs::read(path) else { + log::warn!( + "[desktop] could not read the window icon {}", + path.display() + ); + return; + }; + let Ok(handle) = HasWindowHandle::window_handle(window) else { + return; + }; + let RawWindowHandle::Win32(win32) = handle.as_raw() else { + return; + }; + let hwnd = win32.hwnd.get() as _; + // The icon resource version `CreateIconFromResourceEx` expects for an icon. + const ICON_VERSION: u32 = 0x0003_0000; + for (metric, which) in [(SM_CXICON, ICON_BIG), (SM_CXSMICON, ICON_SMALL)] { + // SAFETY: `bytes` is alive for the call and the length matches it; `hwnd` + // comes from the window's raw handle, which GPUI keeps valid while the + // window lives. The icon is handed to the window, which owns it after that. + unsafe { + let edge = GetSystemMetrics(metric); + let icon = CreateIconFromResourceEx( + bytes.as_ptr(), + bytes.len() as u32, + 1, + ICON_VERSION, + edge, + edge, + LR_DEFAULTCOLOR, + ); + if icon.is_null() { + log::warn!( + "[desktop] could not build the window icon from {}", + path.display() + ); + continue; + } + SendMessageW(hwnd, WM_SETICON, which as usize, icon as isize); + } + } +} + +#[cfg(not(target_os = "windows"))] +pub fn set_window_icon(_window: &gpui::Window, _path: &std::path::Path) {} + +/// Gives a window the app's icon, as a dev run is told it through +/// `RN_GPUI_APP_ICON`. For every window the app opens: the icon is per window on +/// Windows, so the one opened at startup is not enough. A packaged app has its +/// icon in its executable, and the variable is unset. +pub fn apply_app_icon(window: &gpui::Window) { + if let Some(icon) = std::env::var_os("RN_GPUI_APP_ICON") { + set_window_icon(window, std::path::Path::new(&icon)); + } +} + +#[cfg(target_os = "macos")] +pub fn disable_window_animation(window: &mut gpui::Window) { + use cocoa::base::id; + use objc::{msg_send, sel, sel_impl}; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + + const NONE: i64 = 2; + let Ok(handle) = HasWindowHandle::window_handle(window) else { + return; + }; + if let RawWindowHandle::AppKit(appkit) = handle.as_raw() { + // SAFETY: `ns_view` comes from the window's raw handle, which GPUI keeps valid while the window lives. + unsafe { + let ns_view = appkit.ns_view.as_ptr() as id; + let native: id = msg_send![ns_view, window]; + if !native.is_null() { + let _: () = msg_send![native, setAnimationBehavior: NONE]; + } + } + } +} + +#[cfg(not(target_os = "macos"))] +pub fn disable_window_animation(_window: &mut gpui::Window) {} + +#[cfg(target_os = "macos")] +pub fn style_as_popover(window: &mut gpui::Window) { + use cocoa::base::{id, NO, YES}; + use objc::{msg_send, sel, sel_impl}; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + + let Ok(handle) = HasWindowHandle::window_handle(window) else { + return; + }; + if let RawWindowHandle::AppKit(appkit) = handle.as_raw() { + // SAFETY: `ns_view` comes from the window's raw handle, which GPUI keeps valid while the window lives. + unsafe { + let ns_view = appkit.ns_view.as_ptr() as id; + let native: id = msg_send![ns_view, window]; + if native.is_null() { + return; + } + let _: () = msg_send![native, setMovable: NO]; + let _: () = msg_send![native, setMovableByWindowBackground: NO]; + let _: () = msg_send![native, setHasShadow: YES]; + // NSWindowCloseButton, NSWindowMiniaturizeButton, NSWindowZoomButton. + for button in 0..3u64 { + let button: id = msg_send![native, standardWindowButton: button]; + if !button.is_null() { + let _: () = msg_send![button, setHidden: YES]; + } + } + } + } +} + +#[cfg(not(target_os = "macos"))] +pub fn style_as_popover(_window: &mut gpui::Window) {} + +/// `tray-icon` reports physical pixels; windows and menus are placed in points. +#[cfg(target_os = "macos")] +pub fn primary_screen_scale() -> f32 { + use cocoa::base::id; + use objc::{class, msg_send, sel, sel_impl}; + + // SAFETY: `NSScreen` class messages return objects that are null-checked before use. + unsafe { + let screens: id = msg_send![class!(NSScreen), screens]; + let screen: id = msg_send![screens, firstObject]; + if screen.is_null() { + return 1.0; + } + let scale: f64 = msg_send![screen, backingScaleFactor]; + if scale > 0.0 { + scale as f32 + } else { + 1.0 + } + } +} + +/// Windows reports the tray icon's rectangle in physical pixels as well, and the +/// popover is placed in points, so the rectangle has to be divided by the scale +/// of the display the taskbar is on — the primary one. +#[cfg(target_os = "windows")] +pub fn primary_screen_scale() -> f32 { + use windows_sys::Win32::UI::HiDpi::GetDpiForSystem; + // SAFETY: `GetDpiForSystem` takes no arguments and only reads the process's DPI state. + let dpi = unsafe { GetDpiForSystem() }; + if dpi == 0 { + 1.0 + } else { + dpi as f32 / 96.0 + } +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +pub fn primary_screen_scale() -> f32 { + 1.0 +} + +/// The usable area of the display holding the point `(x, y)` — the screen minus +/// the taskbar — as `(x, y, width, height)` in points. `None` where it cannot be +/// asked, and the popover is then left where the icon puts it. +#[cfg(target_os = "windows")] +pub fn work_area_at(x: f32, y: f32) -> Option<(f32, f32, f32, f32)> { + use windows_sys::Win32::Foundation::POINT; + use windows_sys::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromPoint, MONITORINFO, MONITOR_DEFAULTTONEAREST, + }; + let scale = primary_screen_scale(); + let point = POINT { + x: (x * scale).round() as i32, + y: (y * scale).round() as i32, + }; + // SAFETY: `monitor` comes from `MonitorFromPoint` and is checked for null; `info` is a + // properly sized, initialised `MONITORINFO` that `GetMonitorInfoW` fills in. + unsafe { + let monitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST); + if monitor.is_null() { + return None; + } + let mut info: MONITORINFO = std::mem::zeroed(); + info.cbSize = std::mem::size_of::() as u32; + if GetMonitorInfoW(monitor, &mut info) == 0 { + return None; + } + let work = info.rcWork; + Some(( + work.left as f32 / scale, + work.top as f32 / scale, + (work.right - work.left) as f32 / scale, + (work.bottom - work.top) as f32 / scale, + )) + } +} + +#[cfg(not(target_os = "windows"))] +pub fn work_area_at(_x: f32, _y: f32) -> Option<(f32, f32, f32, f32)> { + None +} + +/// What the OS itself has for a window: `(left, top, right, bottom, visible)`, in +/// physical pixels. GPUI's own bounds are what it believes, and for a window +/// created hidden on Windows the two have differed, so a test that wants to know +/// what a user sees asks the OS. +#[cfg(all(target_os = "windows", feature = "test-driver"))] +pub fn native_frame(window: &gpui::Window) -> Option<(i32, i32, i32, i32, bool)> { + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + use windows_sys::Win32::Foundation::RECT; + use windows_sys::Win32::UI::WindowsAndMessaging::{GetWindowRect, IsWindowVisible}; + let handle = HasWindowHandle::window_handle(window).ok()?; + let RawWindowHandle::Win32(win32) = handle.as_raw() else { + return None; + }; + let hwnd = win32.hwnd.get() as _; + let mut rect = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + // SAFETY: `hwnd` comes from the window's raw handle, which GPUI keeps valid while the window + // lives, and `rect` is a properly initialised `RECT` the call fills in. + unsafe { + if GetWindowRect(hwnd, &mut rect) == 0 { + return None; + } + Some(( + rect.left, + rect.top, + rect.right, + rect.bottom, + IsWindowVisible(hwnd) != 0, + )) + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/tray.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/tray.rs new file mode 100644 index 0000000000..bb9d7b2aeb --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/tray.rs @@ -0,0 +1,1106 @@ +//! Windows has no anchored popover: the window opens at the click point. Linux +//! AppIndicator only supports menus, not click-to-popover. + +use crate::bootstrap::WindowTitleBar; +use crate::desktop::errors::{DesktopError, DesktopResult}; +use crate::desktop::menu_model::{self, MenuModel}; +use crate::desktop::windows::{self, OpenWindowOptions, WindowRoleOverride}; +use gpui::{App, Global}; +use serde_json::{json, Value}; +use std::cell::RefCell; +use std::collections::HashSet; + +#[derive(Clone)] +pub struct TrayConfig { + pub id: String, + pub tooltip: Option, + pub title: Option, + /// RGBA pixels for the icon. `None` uses a built-in template glyph. + pub icon: Option<(Vec, u32, u32)>, + pub icon_is_template: bool, + pub component: Option, + pub width: f32, + pub height: f32, + pub menu: Option, + pub menu_on_left_click: bool, + pub quit_on_last_window_closed: bool, + /// Hide the last window when it is closed instead of destroying it, so opening + /// the app again from the tray shows the same window with its state. Off by + /// default: a closed window is gone and a reopened one starts fresh. + pub hide_window_on_close: bool, + pub accessory: bool, +} + +impl Default for TrayConfig { + fn default() -> Self { + Self { + id: "tray".to_string(), + tooltip: None, + title: None, + icon: None, + icon_is_template: true, + component: None, + width: 320.0, + height: 420.0, + menu: None, + menu_on_left_click: false, + quit_on_last_window_closed: false, + hide_window_on_close: false, + accessory: true, + } + } +} + +impl TrayConfig { + #[must_use] + pub fn hide_window_on_close(mut self, hide: bool) -> Self { + self.hide_window_on_close = hide; + self + } + + #[must_use] + pub fn id(mut self, id: impl Into) -> Self { + self.id = id.into(); + self + } + + #[must_use] + pub fn tooltip(mut self, tooltip: impl Into) -> Self { + self.tooltip = Some(tooltip.into()); + self + } + + #[must_use] + pub fn component(mut self, component: impl Into) -> Self { + self.component = Some(component.into()); + self + } + + #[must_use] + pub fn size(mut self, width: f32, height: f32) -> Self { + self.width = width.max(120.0); + self.height = height.max(120.0); + self + } + + #[must_use] + pub fn icon_rgba(mut self, rgba: Vec, width: u32, height: u32) -> Self { + self.icon = Some((rgba, width, height)); + self + } + + /// An encoded image (PNG, JPEG, …), typically from `include_bytes!`. One + /// that does not decode is logged and the built-in glyph is used instead. + #[must_use] + pub fn icon_bytes(mut self, bytes: &[u8]) -> Self { + match crate::desktop::icon::decode(bytes.to_vec()) { + Ok(icon) => self.icon = Some((icon.rgba, icon.width, icon.height)), + Err(error) => log::error!("[desktop][tray] icon: {error}"), + } + self + } + + /// macOS: draw the icon as a template the menu bar tints for light and + /// dark mode. + #[must_use] + pub fn icon_template(mut self, template: bool) -> Self { + self.icon_is_template = template; + self + } + + #[must_use] + pub fn menu(mut self, menu: MenuModel) -> Self { + self.menu = Some(menu); + self + } + + #[must_use] + pub fn accessory(mut self, accessory: bool) -> Self { + self.accessory = accessory; + self + } +} + +pub struct TrayState { + icon: Option, + config: TrayConfig, + pub popover: Option, + visible: bool, + clicks: u32, + pub popover_dismissed_at: Option, + /// The app's own quit-on-last-window policy, put back when the tray goes. + app_quits_on_last_window: Option, +} + +/// A click on the icon while the popover is open first dismisses it (it loses +/// activation) and then arrives here; within this window it must not reopen it. +const REOPEN_GUARD: std::time::Duration = std::time::Duration::from_millis(350); + +pub struct TrayGlobal(pub RefCell); +impl Global for TrayGlobal {} + +pub fn is_installed(cx: &App) -> bool { + cx.try_global::().is_some() +} + +/// Create the tray icon. Called once, after the app has started, because +/// `NSStatusItem` needs a running `NSApplication`. +pub fn install(cx: &mut App, config: TrayConfig) -> DesktopResult { + if is_installed(cx) { + return Err(DesktopError::failed("a tray icon is already installed")); + } + let app_quits_on_last_window = cx + .try_global::() + .map(|flag| flag.0); + cx.set_global(TrayGlobal(RefCell::new(TrayState { + icon: None, + config: config.clone(), + popover: None, + visible: true, + clicks: 0, + popover_dismissed_at: None, + app_quits_on_last_window, + }))); + let quits_on_last_window = + config.quit_on_last_window_closed && app_quits_on_last_window.unwrap_or(true); + cx.set_global(crate::desktop::windows::QuitOnLastWindowClosed( + quits_on_last_window, + )); + // GPUI quits by itself when its last window closes on every platform but macOS, + // and our flag is only consulted after that: with the flag off, closing the + // main window still ended the app, tray and all. While a tray keeps the app + // alive, quitting is explicit — the tray's Exit, or the app asking — and + // `windows::maybe_quit` stays the one place that applies the flag. + cx.set_quit_mode(if quits_on_last_window { + gpui::QuitMode::Default + } else { + gpui::QuitMode::Explicit + }); + match build_icon(&config) { + Ok(icon) => { + attach_native_menu(&icon, &config); + cx.global_mut::().0.borrow_mut().icon = Some(icon); + log::info!("[desktop][tray] installed `{}`", config.id); + Ok(json!({"id": config.id})) + } + Err(error) => { + log::error!("[desktop][tray] could not create the icon: {error}"); + Err(DesktopError::failed(format!( + "could not create the tray icon: {error}" + ))) + } + } +} + +pub fn destroy(cx: &mut App) -> DesktopResult { + let (config, popover, app_quits_on_last_window) = { + let Some(tray) = cx.try_global::() else { + return Err(DesktopError::not_found("no tray icon is installed")); + }; + let mut tray = tray.0.borrow_mut(); + let popover = tray.popover.take(); + tray.icon = None; + NATIVE_MENU.store(0, std::sync::atomic::Ordering::Relaxed); + tray.visible = false; + ( + tray.config.id.clone(), + popover, + tray.app_quits_on_last_window, + ) + }; + if let Some(key) = popover { + let _ = windows::close_window(cx, key, true); + } + cx.remove_global::(); + if let Some(quit) = app_quits_on_last_window { + cx.set_global(crate::desktop::windows::QuitOnLastWindowClosed(quit)); + } + // Without a tray there is nothing left to keep the app alive: if no window is + // open either, the app ends, as it would have when its last window closed. + cx.set_quit_mode(gpui::QuitMode::Default); + windows::maybe_quit(cx); + Ok(json!({"id": config, "destroyed": true})) +} + +pub fn handle_click(cx: &mut App, rect: TrayRect, button: TrayButton) { + let (config, menu) = { + let Some(tray) = cx.try_global::() else { + return; + }; + let mut tray = tray.0.borrow_mut(); + tray.clicks += 1; + (tray.config.clone(), tray.config.menu.clone()) + }; + if wants_menu(menu.is_some(), config.menu_on_left_click, button) { + if let Some(menu) = menu { + show_menu(cx, menu, rect); + } + return; + } + if config.component.is_some() { + toggle_popover(cx, rect); + return; + } + log::debug!("[desktop][tray] click with no popover component and no menu"); +} + +pub fn toggle_popover(cx: &mut App, rect: TrayRect) { + let open = cx + .try_global::() + .and_then(|tray| tray.0.borrow().popover); + if open.is_some() { + hide_popover(cx); + return; + } + let just_dismissed = cx.try_global::().is_some_and(|tray| { + tray.0 + .borrow() + .popover_dismissed_at + .is_some_and(|at| at.elapsed() < REOPEN_GUARD) + }); + if just_dismissed { + return; + } + show_popover(cx, rect); +} + +pub fn show_popover(cx: &mut App, rect: TrayRect) { + let config = match cx.try_global::() { + Some(tray) => tray.0.borrow().config.clone(), + None => return, + }; + let Some(component) = config.component.clone() else { + return; + }; + let work_area = crate::desktop::platform_window::work_area_at( + rect.x + rect.width / 2.0, + rect.y + rect.height / 2.0, + ); + let (x, y) = popover_origin(rect, config.width, config.height, work_area); + let options = OpenWindowOptions { + component, + props: None, + title: Some(config.tooltip.clone().unwrap_or_else(|| config.id.clone())), + width: Some(config.width), + height: Some(config.height), + x: Some(x), + y: Some(y), + resizable: false, + // A popover has no title bar at all; `popup` is what makes it frameless. + title_bar: WindowTitleBar::Default, + traffic_light_position: None, + role: WindowRoleOverride::TrayPopover, + popup: true, + }; + match windows::open_window(cx, options) { + Ok(key) => { + cx.global_mut::().0.borrow_mut().popover = Some(key); + let _ = windows::focus(cx, key); + } + Err(error) => log::error!("[desktop][tray] could not open the popover: {error}"), + } +} + +/// Where a popover of `width` x `height` goes, in points: centred on the icon, +/// below it when there is room — a menu bar at the top of the screen — and above +/// it when there is not, which is where a taskbar at the bottom puts it. +/// +/// `work_area` is `(x, y, width, height)` of the usable part of the display, when +/// the platform can say. Without it the popover goes where the icon puts it, below +/// and centred, as it always did. With it, the popover is also kept inside the +/// area on every side, so an icon near a corner does not push it off the screen. +fn popover_origin( + rect: TrayRect, + width: f32, + height: f32, + work_area: Option<(f32, f32, f32, f32)>, +) -> (f32, f32) { + let x = rect.x + rect.width / 2.0 - width / 2.0; + let below = rect.y + rect.height; + let Some((area_x, area_y, area_width, area_height)) = work_area else { + return (x, below); + }; + let above = rect.y - height; + let fits_below = below + height <= area_y + area_height; + let y = if fits_below || above < area_y { + below + } else { + above + }; + let max_x = (area_x + area_width - width).max(area_x); + let max_y = (area_y + area_height - height).max(area_y); + (x.clamp(area_x, max_x), y.clamp(area_y, max_y)) +} + +pub fn hide_popover(cx: &mut App) { + let open = cx + .try_global::() + .and_then(|tray| tray.0.borrow_mut().popover.take()); + if let Some(key) = open { + // Closed, not hidden: every show opens a new window, and a hidden one + // would stay behind with its surface (and come back on screen when a + // pending activation reaches it). + let _ = windows::close_window(cx, key, true); + } +} + +/// Show `menu` through the context-menu presenter. A real click on macOS and +/// Windows never gets here: the status item shows its attached menu itself. +pub fn show_menu(cx: &mut App, menu: MenuModel, rect: TrayRect) { + let request = crate::desktop::context_menu::ContextMenuRequest { + window: match crate::desktop::context_menu::target_window(cx) { + Ok((window, _)) => window, + Err(error) => { + log::warn!("[desktop][tray] {error}"); + return; + } + }, + x: rect.x, + y: rect.y + rect.height, + model: menu, + native_view: crate::desktop::context_menu::target_window(cx) + .map(|(_, view)| view) + .unwrap_or(0), + }; + let responder = crate::desktop::context_menu::Responder::callback(Box::new(|selection| { + if let Some(id) = selection { + if let Some(handler) = rn_gpui_sys::TRAY_MENU_PERFORM_HANDLER.get() { + handler(id); + } + } + })); + let presenter = cx + .global::() + .0 + .clone(); + presenter.show(cx, request, responder); +} + +pub fn set_tooltip(cx: &mut App, tooltip: Option<&str>) -> DesktopResult { + let tray = installed(cx)?; + let icon = tray.0.borrow().icon.clone(); + if let Some(icon) = icon { + let _ = icon.set_tooltip(tooltip); + } + tray.0.borrow_mut().config.tooltip = tooltip.map(str::to_string); + Ok(json!({"tooltip": tooltip})) +} + +pub fn set_visible(cx: &mut App, visible: bool) -> DesktopResult { + let tray = installed(cx)?; + let icon = tray.0.borrow().icon.clone(); + if let Some(icon) = icon { + let _ = icon.set_visible(visible); + } + tray.0.borrow_mut().visible = visible; + Ok(json!({"visible": visible})) +} + +pub fn set_icon( + cx: &mut App, + icon: &crate::desktop::icon::DecodedIcon, + template: bool, +) -> DesktopResult { + let tray = installed(cx)?; + let native = tray_icon::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) + .map_err(|error| DesktopError::invalid_argument(format!("{error}")))?; + let handle = tray.0.borrow().icon.clone(); + if let Some(handle) = handle { + #[cfg(target_os = "macos")] + let applied = if template { + handle.set_icon_templated(Some(native)) + } else { + handle.set_icon(Some(native)) + }; + #[cfg(not(target_os = "macos"))] + let applied = handle.set_icon(Some(native)); + applied.map_err(|error| DesktopError::failed(format!("{error}")))?; + } + // Kept, so a tray rebuilt from its config shows the same icon. + let mut state = tray.0.borrow_mut(); + state.config.icon = Some((icon.rgba.clone(), icon.width, icon.height)); + state.config.icon_is_template = template; + Ok(json!({"width": icon.width, "height": icon.height, "template": template})) +} + +/// Text beside the icon; Windows trays have none. +pub fn set_title(cx: &mut App, title: Option<&str>) -> DesktopResult { + let tray = installed(cx)?; + let icon = tray.0.borrow().icon.clone(); + if let Some(icon) = icon { + icon.set_title(title); + } + tray.0.borrow_mut().config.title = title.map(str::to_string); + Ok(json!({"title": title})) +} + +/// `None` clears the menu. +pub fn set_menu(cx: &mut App, items: Option<&Value>) -> DesktopResult { + let tray = installed(cx)?; + let menu = match items { + Some(items) => Some(menu_model::parse_menu(items)?), + None => None, + }; + tray.0.borrow_mut().config.menu = menu; + { + let tray = tray.0.borrow(); + if let Some(icon) = &tray.icon { + attach_native_menu(icon, &tray.config); + } + } + Ok(json!({"items": items.and_then(Value::as_array).map(Vec::len)})) +} + +/// The tray global, or the error every setter reports when there is none. +fn installed(cx: &App) -> Result<&TrayGlobal, DesktopError> { + cx.try_global::() + .ok_or_else(|| DesktopError::not_found("no tray icon is installed")) +} + +pub fn state(cx: &App) -> Value { + let Some(tray) = cx.try_global::() else { + return json!({"installed": false}); + }; + let tray = tray.0.borrow(); + json!({ + "installed": true, + "id": tray.config.id, + "tooltip": tray.config.tooltip, + "visible": tray.visible, + "component": tray.config.component, + "popover": tray.popover, + "clicks": tray.clicks, + "hasMenu": tray.config.menu.is_some(), + "hideWindowOnClose": tray.config.hide_window_on_close, + "title": tray.config.title, + "icon": tray.config.icon.as_ref().map(|(_, width, height)| json!({ + "width": width, + "height": height, + "template": tray.config.icon_is_template, + })), + // Whether a StatusNotifier host is watching the session bus, so a caller + // can tell "the icon is there and the panel is showing it" from "the app + // is publishing an icon that this desktop has nowhere to draw". Only + // Linux has the distinction; elsewhere it is always true. + "hostAvailable": tray_host_available(), + "menuItems": tray + .config + .menu + .as_ref() + .map(|menu| crate::desktop::context_menu::nodes_json(&menu.items)) + .unwrap_or_default(), + }) +} + +/// Whether a StatusNotifier host owns `org.kde.StatusNotifierWatcher`. +/// +/// The tray icon is published over StatusNotifierItem; a panel (or a +/// GNOME AppIndicator extension) has to be *hosting* that protocol for anything +/// to appear. A desktop that does not host it — stock GNOME, or an LXQt panel +/// built without its `statusnotifier` plugin — shows no icon for any +/// StatusNotifierItem app (ours, rustdesk, an Electron app), which is worth +/// reporting rather than looking like a failure in the app. Some desktops +/// (Electron included) additionally keep the retired XEmbed tray, but no +/// maintained crate offers that without GTK, and it is absent from GNOME 3.26+ +/// and from the X11-less Wayland sessions. +/// +/// Reads the session bus with `zbus` (already a dependency, via `ksni`). The +/// query is synchronous and cheap, but only consulted when a caller asks for +/// the tray state, never on a timer. +#[cfg(target_os = "linux")] +fn tray_host_available() -> bool { + use zbus::blocking::Connection; + let Ok(connection) = Connection::session() else { + return false; + }; + connection + .call_method( + Some("org.freedesktop.DBus"), + "/org/freedesktop/DBus", + Some("org.freedesktop.DBus"), + "NameHasOwner", + &("org.kde.StatusNotifierWatcher",), + ) + .ok() + .and_then(|reply| reply.body().deserialize::().ok()) + .unwrap_or(false) +} + +#[cfg(not(target_os = "linux"))] +fn tray_host_available() -> bool { + true +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct TrayRect { + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TrayButton { + Left, + Right, + Middle, +} + +fn build_icon(config: &TrayConfig) -> Result { + use tray_icon::{TrayIconBuilder, TrayIconEvent}; + + let icon = match &config.icon { + Some((rgba, width, height)) => tray_icon::Icon::from_rgba(rgba.clone(), *width, *height) + .map_err(|error| error.to_string())?, + None => fallback_icon()?, + }; + let mut builder = TrayIconBuilder::new() + .with_id(config.id.clone()) + .with_menu_on_left_click(false); + // Template mode is a macOS menu-bar concept; every other platform draws + // the icon as-is. The built-in glyph is always a template. + #[cfg(target_os = "macos")] + { + builder = if config.icon.is_none() || config.icon_is_template { + builder.with_icon_templated(icon) + } else { + builder.with_icon(icon) + }; + } + #[cfg(not(target_os = "macos"))] + { + builder = builder.with_icon(icon); + } + if let Some(tooltip) = &config.tooltip { + builder = builder.with_tooltip(tooltip.clone()); + } + if let Some(title) = &config.title { + builder = builder.with_title(title.clone()); + } + let tray = builder.build().map_err(|error| error.to_string())?; + + // The handler runs on the platform's event thread; the click is forwarded + // to the GPUI main thread through the shared queue. + TrayIconEvent::set_event_handler(Some(move |event| { + // One click is a press and a release, and each is its own event; acting + // on both would open the popover and close it again. + let (rect, position, button) = match event { + TrayIconEvent::Click { + rect, + position, + button, + button_state: tray_icon::MouseButtonState::Up, + .. + } => (rect, position, button), + _ => return, + }; + let button = match button { + tray_icon::MouseButton::Left => TrayButton::Left, + tray_icon::MouseButton::Right => TrayButton::Right, + tray_icon::MouseButton::Middle => TrayButton::Middle, + }; + // The status item shows an attached menu itself, anchored to the icon; + // forwarding that click as well would show it a second time. The Linux + // backend also reports a right-click, but the panel has already put the + // D-Bus menu up by then, so it is dropped for the same reason. + let native_menu = NATIVE_MENU.load(std::sync::atomic::Ordering::Relaxed); + if native_menu & MENU_ATTACHED != 0 + && (button == TrayButton::Right || native_menu & MENU_ON_LEFT_CLICK != 0) + { + return; + } + // `tray-icon` reports physical pixels; everything here is in points. + let scale = f64::from(crate::desktop::platform_window::primary_screen_scale()); + let rect = tray_click_rect(rect, position, scale); + if let Some(handler) = rn_gpui_sys::TRAY_CLICK_HANDLER.get() { + handler(rect.x, rect.y, rect.width, rect.height, button as u8); + } + })); + Ok(tray) +} + +/// The icon's rectangle in points, from what a backend reported. +/// +/// macOS and Windows report the icon's own rect, so it is used directly. The +/// Linux `ksni` backend has no rect to report — the panel owns the icon — so it +/// fills `rect` with zeros and puts the click's screen position in `position` +/// (`platform_impl/ksni/mod.rs`). Falling back to the position keeps the popover +/// at the click instead of centred on the top-left corner (0,0). Some panels +/// report `(0,0)` even then; the pointer's own position is the last resort. +fn tray_click_rect( + rect: tray_icon::Rect, + position: tray_icon::dpi::PhysicalPosition, + scale: f64, +) -> TrayRect { + if rect.size.width > 0 || rect.size.height > 0 { + return TrayRect { + x: (rect.position.x / scale) as f32, + y: (rect.position.y / scale) as f32, + width: (f64::from(rect.size.width) / scale) as f32, + height: (f64::from(rect.size.height) / scale) as f32, + }; + } + let (x, y) = if position.x != 0.0 || position.y != 0.0 { + (position.x, position.y) + } else { + pointer_position().unwrap_or((0.0, 0.0)) + }; + TrayRect { + x: (x / scale) as f32, + y: (y / scale) as f32, + width: 0.0, + height: 0.0, + } +} + +/// The pointer's screen position in physical pixels, where the platform can +/// answer. Only X11 needs it (see `tray_click_rect`); elsewhere the backend +/// always reports a real rect. +#[cfg(target_os = "linux")] +fn pointer_position() -> Option<(f64, f64)> { + let xlib = x11_dl::xlib::Xlib::open().ok()?; + // SAFETY: the display from `XOpenDisplay` is null-checked before use and closed before returning. + unsafe { + let display = (xlib.XOpenDisplay)(std::ptr::null()); + if display.is_null() { + return None; + } + let root = (xlib.XDefaultRootWindow)(display); + let mut root_return = 0; + let mut child_return = 0; + let mut root_x = 0; + let mut root_y = 0; + let mut win_x = 0; + let mut win_y = 0; + let mut mask = 0; + let found = (xlib.XQueryPointer)( + display, + root, + &mut root_return, + &mut child_return, + &mut root_x, + &mut root_y, + &mut win_x, + &mut win_y, + &mut mask, + ); + (xlib.XCloseDisplay)(display); + if found != 0 { + Some((root_x as f64, root_y as f64)) + } else { + None + } + } +} + +#[cfg(not(target_os = "linux"))] +fn pointer_position() -> Option<(f64, f64)> { + None +} + +/// Whether the status item has a native menu, and which button opens it. Read +/// by the click handler, which runs on the platform's event thread. +static NATIVE_MENU: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0); +const MENU_ATTACHED: u8 = 1; +const MENU_ON_LEFT_CLICK: u8 = 2; + +/// Attaches the menu the platform shows itself, anchored to the icon. +/// +/// Every platform does this through `tray-icon`: macOS and Windows build a +/// native menu, and Linux's `ksni` backend turns the *same* `muda` menu into a +/// D-Bus `com.canonical.dbusmenu` the panel draws (the panel owns the popup, so +/// there is no separate window for us to place). `tray-icon` re-exports the +/// matching `muda`, so one builder serves all three. +fn attach_native_menu(icon: &tray_icon::TrayIcon, config: &TrayConfig) { + use std::sync::atomic::Ordering; + use tray_icon::menu::Menu; + + let fallback = default_menu(); + let model = match &config.menu { + Some(model) => Some(model), + None if default_menu_applies() => Some(&fallback), + None => None, + }; + let Some(model) = model else { + icon.set_menu(None); + *TRAY_MENU_IDS + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = HashSet::new(); + NATIVE_MENU.store(0, Ordering::Relaxed); + return; + }; + let menu = Menu::new(); + if let Err(error) = build_tray_menu(&menu, &model.items) { + log::error!("[desktop][tray] could not build the menu: {error}"); + icon.set_menu(None); + *TRAY_MENU_IDS + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = HashSet::new(); + NATIVE_MENU.store(0, Ordering::Relaxed); + return; + } + icon.set_menu(Some(Box::new(menu))); + icon.set_show_menu_on_left_click(config.menu_on_left_click); + NATIVE_MENU.store( + MENU_ATTACHED + | if config.menu_on_left_click { + MENU_ON_LEFT_CLICK + } else { + 0 + }, + Ordering::Relaxed, + ); + // The ids this menu answers for, so the app's single `muda` event route can + // tell a tray selection from a menu-bar one. `tray_icon` forwards mudas + // events, and the app installs one handler on that channel for every menu it + // builds; a second installer here would be ignored (the setter is + // first-caller-wins) and the tray would then never be called. + let mut ids = HashSet::new(); + collect_menu_ids(&model.items, &mut ids); + *TRAY_MENU_IDS + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = ids; + crate::desktop::menu_events::register(Box::new(|id| { + let owned = TRAY_MENU_IDS + .lock() + .map(|ids| ids.contains(id)) + .unwrap_or(false); + if !owned { + return false; + } + if let Some(handler) = rn_gpui_sys::TRAY_MENU_PERFORM_HANDLER.get() { + handler(id.to_string()); + return true; + } + false + })); +} + +/// Every id the tray menu answers for, so a tray selection can be told from a +/// menu-bar one on the shared event route. +fn collect_menu_ids(items: &[menu_model::MenuNode], ids: &mut HashSet) { + use menu_model::MenuNode; + for node in items { + match node { + MenuNode::Item(item) => { + ids.insert(item.id.clone()); + } + MenuNode::Submenu(submenu) => collect_menu_ids(&submenu.items, ids), + MenuNode::Separator | MenuNode::StandardMenu(_) => {} + } + } +} + +/// The ids of the tray menu currently attached, if any. +static TRAY_MENU_IDS: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| std::sync::Mutex::new(HashSet::new())); + +trait TrayMenuAppend { + fn append_all(&self, items: &[&dyn tray_icon::menu::IsMenuItem]) + -> tray_icon::menu::Result<()>; +} + +impl TrayMenuAppend for tray_icon::menu::Menu { + fn append_all( + &self, + items: &[&dyn tray_icon::menu::IsMenuItem], + ) -> tray_icon::menu::Result<()> { + self.append_items(items) + } +} + +impl TrayMenuAppend for tray_icon::menu::Submenu { + fn append_all( + &self, + items: &[&dyn tray_icon::menu::IsMenuItem], + ) -> tray_icon::menu::Result<()> { + self.append_items(items) + } +} + +/// The tray's menu in `tray-icon`'s own menu types. It bundles a newer `muda` +/// than the context menus use, so `context_menu`'s builder cannot be shared. +fn build_tray_menu( + menu: &impl TrayMenuAppend, + items: &[menu_model::MenuNode], +) -> Result<(), String> { + use menu_model::MenuNode; + use tray_icon::menu::{ + CheckMenuItem, IconMenuItem, IsMenuItem, MenuItem, PredefinedMenuItem, Submenu, + }; + let icon = |uri: &Option| { + let icon = crate::desktop::icon::cached(uri.as_deref()?)?; + tray_icon::menu::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height).ok() + }; + + let mut children: Vec> = Vec::new(); + for node in items { + match node { + MenuNode::Separator => children.push(Box::new(PredefinedMenuItem::separator())), + MenuNode::StandardMenu(_) => {} + MenuNode::Item(item) if item.checked => { + children.push(Box::new(CheckMenuItem::with_id( + item.id.clone(), + item.label.clone(), + item.enabled, + true, + None, + ))) + } + MenuNode::Item(item) => match icon(&item.icon) { + Some(icon) => children.push(Box::new(IconMenuItem::with_id( + item.id.clone(), + item.label.clone(), + item.enabled, + Some(icon), + None, + ))), + None => children.push(Box::new(MenuItem::with_id( + item.id.clone(), + item.label.clone(), + item.enabled, + None, + ))), + }, + MenuNode::Submenu(submenu) => { + let child = Submenu::new(submenu.label.clone(), submenu.enabled); + if let Some(icon) = icon(&submenu.icon) { + child.set_icon(Some(icon)); + } + build_tray_menu(&child, &submenu.items)?; + children.push(Box::new(child)); + } + } + } + let refs: Vec<&dyn IsMenuItem> = children.iter().map(|item| item.as_ref()).collect(); + menu.append_all(&refs).map_err(|error| error.to_string()) +} + +/// The icon for a tray that was not given one: the app's own where the platform +/// draws a tray icon as-is, and the built-in glyph where it does not. +/// +/// The glyph is a macOS template — a black outline the menu bar tints for light +/// and dark — so anywhere else it is an empty black rectangle, which on a dark +/// taskbar is hardly there at all. The app's icon is what a user expects to see +/// for the app. +fn fallback_icon() -> Result { + #[cfg(not(target_os = "macos"))] + if let Some(icon) = crate::desktop::icon::app_icon() { + return tray_icon::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) + .map_err(|error| error.to_string()); + } + default_icon() +} + +fn default_icon() -> Result { + const SIZE: u32 = 22; + let mut rgba = vec![0u8; (SIZE * SIZE * 4) as usize]; + for y in 0..SIZE { + for x in 0..SIZE { + let outer = x >= 3 && x < SIZE - 3 && y >= 4 && y < SIZE - 4; + let inner = x >= 6 && x < SIZE - 6 && y >= 7 && y < SIZE - 7; + if outer && !inner { + let index = ((y * SIZE + x) * 4) as usize; + rgba[index] = 0; + rgba[index + 1] = 0; + rgba[index + 2] = 0; + rgba[index + 3] = 255; + } + } + } + tray_icon::Icon::from_rgba(rgba, SIZE, SIZE).map_err(|error| error.to_string()) +} + +fn wants_menu(has_menu: bool, menu_on_left_click: bool, button: TrayButton) -> bool { + has_menu && (button == TrayButton::Right || menu_on_left_click) +} + +/// Whether the installed tray asked for the last window to be hidden, not +/// destroyed, when it is closed. +pub fn hides_window_on_close(cx: &App) -> bool { + cx.try_global::() + .is_some_and(|tray| tray.0.borrow().config.hide_window_on_close) +} + +/// The id of the built-in menu's "Open" item. +const DEFAULT_OPEN_ID: &str = "rn-gpui:tray:open"; +/// The id of the built-in menu's "Exit" item. +const DEFAULT_EXIT_ID: &str = "rn-gpui:tray:exit"; + +/// What a right-click on a tray icon shows when the app gave it no menu: Open and +/// Exit, which is what users expect of a tray icon on Windows and Linux. Without +/// it there is no way to bring a closed window back, or to end an app that keeps +/// running in the background, short of the task manager. +fn default_menu() -> MenuModel { + use menu_model::{MenuItemDef, MenuNode}; + let item = |id: &str, label: &str| { + MenuNode::Item(MenuItemDef { + id: id.to_string(), + label: label.to_string(), + accelerator: None, + enabled: true, + checked: false, + role: None, + icon: None, + }) + }; + MenuModel { + items: vec![ + item(DEFAULT_OPEN_ID, "Open"), + MenuNode::Separator, + item(DEFAULT_EXIT_ID, "Exit"), + ], + } +} + +/// macOS status items do not carry an Open/Exit menu by convention — the Dock and +/// the app menu do that — so only the other platforms get the built-in one. +fn default_menu_applies() -> bool { + cfg!(not(target_os = "macos")) +} + +/// A selection from the tray's menu. The built-in items act here; everything else +/// is the app's, and reaches JavaScript. +pub fn perform_menu(cx: &mut App, id: &str) { + match id { + DEFAULT_OPEN_ID => windows::reopen(cx), + DEFAULT_EXIT_ID => cx.quit(), + _ => emit_menu_press(cx, id), + } +} + +pub fn emit_menu_press(cx: &App, id: &str) { + let Some(store) = crate::store::store_from(cx) else { + return; + }; + store + .read(cx) + .host + .emit_device_event_json("gpuiDesktop:trayMenuPress", &json!({"id": id}).to_string()); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_default_icon_is_a_valid_template() { + let icon = default_icon().expect("the built-in glyph is valid"); + let _ = icon; + } + + #[test] + fn a_tray_config_defaults_to_a_popover_with_no_menu() { + let config = TrayConfig::default(); + assert!(config.component.is_none()); + assert!(config.menu.is_none()); + assert!(!config.quit_on_last_window_closed); + assert_eq!(config.id, "tray"); + } + + #[test] + fn a_right_click_opens_the_menu_only_when_one_exists() { + assert!(wants_menu(true, false, TrayButton::Right)); + assert!(!wants_menu(false, false, TrayButton::Right)); + assert!(!wants_menu(true, false, TrayButton::Left)); + assert!(!wants_menu(true, false, TrayButton::Middle)); + } + + #[test] + fn menu_on_left_click_moves_the_menu_to_the_primary_button() { + assert!(wants_menu(true, true, TrayButton::Left)); + assert!(wants_menu(true, true, TrayButton::Right)); + assert!(!wants_menu(false, true, TrayButton::Left)); + } + + #[test] + fn the_built_in_menu_offers_open_and_exit() { + let menu = default_menu(); + assert_eq!(menu.ids(), [DEFAULT_OPEN_ID, DEFAULT_EXIT_ID]); + assert_eq!( + menu.find(DEFAULT_OPEN_ID).map(|i| i.label.as_str()), + Some("Open") + ); + assert_eq!( + menu.find(DEFAULT_EXIT_ID).map(|i| i.label.as_str()), + Some("Exit") + ); + assert!(menu.find(DEFAULT_OPEN_ID).is_some_and(|item| item.enabled)); + } + + #[test] + fn the_built_in_ids_cannot_be_an_apps_own() { + // They would be swallowed by `perform_menu` instead of reaching the app. + assert!(DEFAULT_OPEN_ID.starts_with("rn-gpui:")); + assert!(DEFAULT_EXIT_ID.starts_with("rn-gpui:")); + assert_ne!(DEFAULT_OPEN_ID, DEFAULT_EXIT_ID); + } + + const ICON: TrayRect = TrayRect { + x: 1200.0, + y: 740.0, + width: 24.0, + height: 28.0, + }; + // A 1280x768 display whose bottom 40 points are the taskbar. + const WORK_AREA: Option<(f32, f32, f32, f32)> = Some((0.0, 0.0, 1280.0, 728.0)); + + #[test] + fn without_a_work_area_a_popover_goes_below_and_centred_as_before() { + assert_eq!( + popover_origin(ICON, 320.0, 200.0, None), + (1200.0 + 12.0 - 160.0, 768.0) + ); + } + + #[test] + fn a_taskbar_at_the_bottom_puts_the_popover_above_it() { + // Below the icon is the bottom of the screen; above has room. The icon + // is inside the taskbar, so the popover rests on the work area's edge + // (728), not on the icon (740) — that would overlap the taskbar. + let (_, y) = popover_origin(ICON, 320.0, 200.0, WORK_AREA); + assert_eq!(y, 728.0 - 200.0); + } + + #[test] + fn a_menu_bar_at_the_top_puts_the_popover_below_its_icon() { + let icon = TrayRect { + x: 600.0, + y: 0.0, + width: 24.0, + height: 24.0, + }; + let (x, y) = popover_origin(icon, 320.0, 200.0, Some((0.0, 24.0, 1280.0, 744.0))); + assert_eq!((x, y), (600.0 + 12.0 - 160.0, 24.0)); + } + + #[test] + fn the_popover_stays_inside_the_work_area_at_a_corner() { + let (x, _) = popover_origin(ICON, 320.0, 200.0, WORK_AREA); + // Centring on an icon 76 points from the right edge would overhang it. + assert_eq!(x, 1280.0 - 320.0); + let corner = TrayRect { + x: 0.0, + y: 0.0, + width: 20.0, + height: 20.0, + }; + assert_eq!( + popover_origin(corner, 320.0, 200.0, Some((0.0, 0.0, 1280.0, 768.0))), + (0.0, 20.0) + ); + } + + #[test] + fn a_popover_taller_than_the_room_is_kept_at_the_top_of_the_area() { + let (_, y) = popover_origin(ICON, 320.0, 900.0, WORK_AREA); + assert_eq!(y, 0.0); + } + + #[test] + fn the_builder_sizes_are_clamped() { + let config = TrayConfig::default().size(10.0, 10.0); + assert_eq!(config.width, 120.0); + assert_eq!(config.height, 120.0); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/windows.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/windows.rs new file mode 100644 index 0000000000..4d06503619 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/desktop/windows.rs @@ -0,0 +1,1042 @@ +//! One `ViewStore` owns every window: Fabric tags are unique across the +//! surfaces of a React instance, so nodes stay in one map. + +use crate::bootstrap::WindowTitleBar; +use crate::desktop::errors::{DesktopError, DesktopResult}; +use crate::desktop::platform_window; +use crate::store::{store_from, StoreGlobal, SurfaceRootView}; +use gpui::{ + point, px, size, AnyWindowHandle, App, AppContext, Bounds, Global, Subscription, + TitlebarOptions, WindowBounds, WindowDecorations, WindowKind, WindowOptions, +}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; +use std::collections::HashMap; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WindowRole { + Main, + Secondary, + TrayPopover, +} + +pub struct WindowEntry { + pub key: u32, + pub handle: AnyWindowHandle, + pub surface_id: i32, + pub component: String, + pub props: Value, + pub role: WindowRole, + pub intercept_close: bool, + pub title: String, + pub subscriptions: Vec, +} + +pub struct DesktopWindows { + by_key: BTreeMap, + by_surface: HashMap, + next_key: u32, + next_surface: i32, +} + +pub struct DesktopWindowsGlobal(pub DesktopWindows); +impl Global for DesktopWindowsGlobal {} + +impl DesktopWindows { + pub fn new() -> Self { + Self { + by_key: BTreeMap::new(), + by_surface: HashMap::new(), + next_key: 2, + next_surface: 3, + } + } + + fn alloc_key(&mut self) -> u32 { + let key = self.next_key; + self.next_key += 1; + key + } + + /// Surface ids are odd. A surface id is also the tag of its root view, and + /// React numbers the views it creates 2, 4, 6…: an even surface id is the + /// tag of some view of another surface, and the new window's content is + /// then mounted inside that view instead of in its own window. + fn alloc_surface(&mut self) -> i32 { + let surface = self.next_surface; + self.next_surface += 2; + surface + } + + pub fn get(&self, key: u32) -> Option<&WindowEntry> { + self.by_key.get(&key) + } + + pub fn get_mut(&mut self, key: u32) -> Option<&mut WindowEntry> { + self.by_key.get_mut(&key) + } + + pub fn entry_for_surface(&self, surface_id: i32) -> Option<&WindowEntry> { + self.by_surface + .get(&surface_id) + .and_then(|key| self.by_key.get(key)) + } + + pub fn keys(&self) -> Vec { + self.by_key.keys().copied().collect() + } + + pub fn len(&self) -> usize { + self.by_key.len() + } + + pub fn adopt( + &mut self, + key: u32, + handle: AnyWindowHandle, + surface_id: i32, + component: String, + props: Value, + role: WindowRole, + ) { + self.by_key.insert( + key, + WindowEntry { + key, + handle, + surface_id, + component, + props, + role, + intercept_close: false, + title: String::new(), + subscriptions: Vec::new(), + }, + ); + self.by_surface.insert(surface_id, key); + } + + fn insert(&mut self, entry: WindowEntry) { + self.by_surface.insert(entry.surface_id, entry.key); + self.by_key.insert(entry.key, entry); + } + + fn remove(&mut self, key: u32) -> Option { + let entry = self.by_key.remove(&key)?; + self.by_surface.remove(&entry.surface_id); + Some(entry) + } +} + +impl Default for DesktopWindows { + fn default() -> Self { + Self::new() + } +} + +pub const MAIN_WINDOW_KEY: u32 = 1; + +#[derive(Clone, Debug, Default)] +pub struct OpenWindowOptions { + pub component: String, + pub props: Option, + pub title: Option, + pub width: Option, + pub height: Option, + pub x: Option, + pub y: Option, + pub resizable: bool, + /// What kind of title bar the window asks for. `Default` keeps the + /// platform's, which is what a secondary window gets unless it says + /// otherwise. + pub title_bar: WindowTitleBar, + pub traffic_light_position: Option<(f32, f32)>, + pub role: WindowRoleOverride, + pub popup: bool, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum WindowRoleOverride { + #[default] + Secondary, + TrayPopover, +} + +impl OpenWindowOptions { + fn role(&self) -> WindowRole { + match self.role { + WindowRoleOverride::Secondary => WindowRole::Secondary, + WindowRoleOverride::TrayPopover => WindowRole::TrayPopover, + } + } +} + +pub fn open_window(cx: &mut App, mut options: OpenWindowOptions) -> Result { + if options.component.is_empty() { + return Err(DesktopError::invalid_argument( + "window.open requires a component name", + )); + } + let store = + store_from(cx).ok_or_else(|| DesktopError::failed("the view store is not initialized"))?; + let (host, frame_hook) = { + let store = store.read(cx); + (store.host.clone(), store.frame_hook.clone()) + }; + let (key, surface_id) = { + let windows = &mut cx.global_mut::().0; + (windows.alloc_key(), windows.alloc_surface()) + }; + + let width = options.width.unwrap_or(800.0).max(120.0); + let height = options.height.unwrap_or(600.0).max(120.0); + let bounds = match (options.x, options.y) { + (Some(x), Some(y)) => Bounds::new(point(px(x), px(y)), size(px(width), px(height))), + _ => Bounds::centered(None, size(px(width), px(height)), cx), + }; + // The placed origin, in points: what the popover's own window hints use. + let placed_origin = (f32::from(bounds.origin.x), f32::from(bounds.origin.y)); + let title = options + .title + .clone() + .unwrap_or_else(|| options.component.clone()); + let window_options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + // A popup (a tray popover) is a bare panel: with a title bar it is + // just another window that happens to open under the icon. + titlebar: (!options.popup).then(|| TitlebarOptions { + title: Some(title.clone().into()), + // macOS and Windows express "the app draws the bar" as a transparent + // title bar. Linux has no such flag and uses the decorations request + // below instead. + appears_transparent: options.title_bar.is_custom(), + traffic_light_position: options.traffic_light_position.map(|(x, y)| { + let (x, y) = crate::bootstrap::clamp_traffic_light_position(x, y, width, height); + point(px(x), px(y)) + }), + }), + is_resizable: options.resizable, + is_movable: !options.popup, + window_min_size: Some(size(px(120.0), px(120.0))), + // The tray popover is a normal, undecorated window on every platform, + // not `WindowKind::PopUp`: GPUI makes that override-redirect on X11, so + // the window manager never focuses it and it can never see the + // deactivation that dismisses it (see `platform_window::prepare_popover`). + // A frameless window with no decorations is what Electron's Linux tray + // menu uses too. `WindowDecorations::Client` is GPUI's frameless mode. + // A custom title bar asks for the same thing: on X11 and Wayland it is + // what removes the window manager's frame, and the app draws both the bar + // and the resize border itself. + kind: WindowKind::Normal, + window_decorations: (options.popup || options.title_bar.is_custom()) + .then_some(WindowDecorations::Client), + // The popover is mapped after it is prepared: the X11 window manager + // reads `_NET_WM_WINDOW_TYPE`, `_NET_WM_STATE` and the size hints when + // the window is mapped, so they are set on it while it is still hidden. + show: !options.popup, + // A custom title bar means the app draws and moves the bar itself. + app_owns_titlebar_drag: options.title_bar.is_custom(), + ..Default::default() + }; + + let store_for_root = store; + let handle = cx + .open_window(window_options, move |window, cx| { + let root = cx.new(|_| SurfaceRootView::new(surface_id)); + let window_handle: AnyWindowHandle = window.window_handle().into(); + store_for_root.update(cx, |store, cx| { + store.register_root(surface_id, root.clone()); + store.register_window(surface_id, window_handle); + cx.notify(); + }); + root + }) + .map_err(|error| DesktopError::failed(format!("could not open the window: {error}")))?; + + let (w, h, scale) = handle + .update(cx, |_root, window, _cx| { + // A compositor can refuse the decorations request for one window and + // grant it for another, so the capability follows the newest window. + crate::desktop::capabilities::note_window_decorations(window.window_decorations()); + let size = window.viewport_size(); + ( + f32::from(size.width), + f32::from(size.height), + window.scale_factor(), + ) + }) + .unwrap_or((width, height, 2.0)); + // A popover is created hidden, and a window that has not been shown has not + // had its first `WM_SIZE`, so what `viewport_size` reports for it is not + // something to start a surface from. The popover's own size is known, so the + // surface starts at that, and `prepare_popover` gives the window the same one. + #[cfg(target_os = "windows")] + let (w, h) = if options.popup { + (width, height) + } else { + (w, h) + }; + + let mut props = options.props.clone().unwrap_or_else(|| json!({})); + if !props.is_object() { + return Err(DesktopError::invalid_argument( + "window.open props must be an object", + )); + } + if let Some(object) = props.as_object_mut() { + object.insert("windowId".to_string(), json!(key)); + } + host.start_surface_with_props( + surface_id, + &options.component, + &props.to_string(), + w, + h, + scale, + ); + + let subscriptions = handle + .update(cx, |_root, window, cx| { + let mut subscriptions = Vec::new(); + if options.popup { + crate::desktop::platform_window::style_as_popover(window); + // Set the popover's window hints while it is still unmapped, + // then ask the window manager to map and focus it. Failures are + // not fatal: a platform that needs none of this (or a Wayland + // session, which has no absolute placement) still gets a working + // window, just placed by the compositor. + if let Err(error) = crate::desktop::platform_window::prepare_popover( + window, + placed_origin.0, + placed_origin.1, + width, + height, + ) { + log::debug!("[desktop] popover window hints: {error}"); + } + } else { + // Every window of the app carries its icon, not only the first: + // on Windows it belongs to the window, so one opened later — a + // reopen from the tray, a secondary window — would show the + // default on its taskbar button. A popover has no button. + platform_window::apply_app_icon(window); + } + frame_hook.arm(window); + subscriptions.push(install_bounds_observer(window, cx)); + subscriptions.push(install_state_observer(window, cx)); + subscriptions.push(install_activation_observer(window, cx, key)); + install_close_hook(window, cx, key); + subscriptions + }) + .map_err(|error| { + DesktopError::failed(format!("could not configure the window: {error}")) + })?; + + if options.popup { + // Map the popover and make it active, so it takes focus (and therefore + // can be dismissed when it loses it). The tray's own `focus` call + // already activates; this uses the pager source indication the window + // manager trusts, which is needed because a tray click arrives over + // D-Bus and carries no X timestamp. + let _ = handle.update(cx, |_root, window, _cx| { + window.activate_window(); + if let Err(error) = crate::desktop::platform_window::activate_popover(window) { + log::debug!("[desktop] popover activation: {error}"); + } + }); + } + + let role = options.role(); + let component = options.component.clone(); + let entry = WindowEntry { + key, + handle: handle.into(), + surface_id, + component, + props: options.props.take().unwrap_or_else(|| json!({})), + role, + intercept_close: false, + title, + subscriptions, + }; + cx.global_mut::().0.insert(entry); + log::info!( + "[desktop] opened window {key} (surface {surface_id}, {})", + options.component + ); + emit_window_event(cx, "gpuiDesktop:windowOpened", json!({"windowId": key})); + Ok(key) +} + +/// Without `force`, a window whose JS listener registered a veto emits +/// `closeRequested` and stays open. +pub fn close_window(cx: &mut App, key: u32, force: bool) -> DesktopResult { + let entry = { + let windows = &mut cx.global_mut::().0; + windows.remove(key) + }; + let Some(entry) = entry else { + return Err(DesktopError::not_found(format!("window {key} is not open"))); + }; + if !force && entry.intercept_close && entry.role != WindowRole::TrayPopover { + // Put it back and ask JS: the veto listener answers with + // `DesktopWindow.close({force: true})` or does nothing to keep it open. + let surface_id = entry.surface_id; + cx.global_mut::().0.insert(entry); + if let Some(store) = store_from(cx) { + let store = store.read(cx); + store.host.emit_device_event_json( + "gpuiDesktop:closeRequested", + &json!({"windowId": key}).to_string(), + ); + log::info!("[desktop] window {key} close deferred to JS (surface {surface_id})"); + } + return Ok(json!({"windowId": key, "deferred": true})); + } + finish_close(cx, key, entry, true); + Ok(json!({"windowId": key})) +} + +/// `remove_window` is false when the platform is already closing the window: +/// GPUI must not be asked to remove it again from inside that callback. +fn finish_close(cx: &mut App, key: u32, entry: WindowEntry, remove_window: bool) { + let surface_id = entry.surface_id; + let handle = entry.handle; + if let Some(store) = store_from(cx) { + // Only the registry entry is dropped here. `stop_surface` goes through + // Fabric's `SurfaceManager`, which can re-enter the JS thread while this + // update holds the store's lease; a mount batch for the surface that is + // still in the channel then has to wait for a lease that cannot be + // released until the stop returns. Deferring the stop lets queued + // batches apply first, which is also the order React Native expects: + // stop is the last thing a closing surface sees. + store.update(cx, |store, _cx| store.unregister_window(surface_id)); + let host = store.read(cx).host.clone(); + cx.defer(move |_cx| host.stop_surface(surface_id)); + } + if remove_window { + let _ = handle.update(cx, |_root, window, _cx| window.remove_window()); + } + log::info!("[desktop] closed window {key} (surface {surface_id})"); + emit_window_event(cx, "gpuiDesktop:windowClosed", json!({"windowId": key})); + // The closed window may have been covering its siblings; repaint them so no + // stale pixels are left where it used to be. + cx.refresh_windows(); + maybe_quit(cx); +} + +pub(crate) fn maybe_quit(cx: &mut App) { + if !quit_on_last_window_closed(cx) { + return; + } + let remaining = cx + .try_global::() + .map(|windows| windows.0.len()) + .unwrap_or(0); + if remaining == 0 { + cx.quit(); + } +} + +pub fn os_close(cx: &mut App, key: u32) -> bool { + let entry = if cx.has_global::() { + cx.global_mut::().0.remove(key) + } else { + None + }; + let Some(entry) = entry else { + return false; + }; + finish_close(cx, key, entry, false); + true +} + +pub fn install_close_hook_for(cx: &mut App, handle: AnyWindowHandle, key: u32) { + // `AnyWindowHandle::update` hands back an `AnyView`; the hook needs the + // window's own context, so the handle is downcast to the root view first. + let Some(handle) = handle.downcast::() else { + return; + }; + let _ = handle.update(cx, |_root, window, cx| { + install_close_hook(window, cx, key); + }); +} + +fn quit_on_last_window_closed(cx: &App) -> bool { + !cx.try_global::() + .is_some_and(|flag| !flag.0) +} + +pub struct QuitOnLastWindowClosed(pub bool); +impl Global for QuitOnLastWindowClosed {} + +/// The window reopening the app opens once its last window is gone: the main +/// component, as the main window was configured. +pub struct ReopenWindow(pub OpenWindowOptions); +impl Global for ReopenWindow {} + +/// The Dock icon was clicked (or the app launched again) with no visible +/// window. A window that is only hidden or minimized is brought back; with none +/// left, the main component opens in a new one. +pub fn reopen(cx: &mut App) { + let existing = cx.try_global::().and_then(|windows| { + windows + .0 + .by_key + .values() + .find(|entry| entry.role != WindowRole::TrayPopover) + .map(|entry| entry.key) + }); + if let Some(key) = existing { + let _ = show(cx, key); + let _ = focus(cx, key); + return; + } + let Some(options) = cx.try_global::().map(|spec| spec.0.clone()) else { + return; + }; + match open_window(cx, options) { + Ok(key) => { + let _ = focus(cx, key); + } + Err(error) => log::error!("[desktop] reopen: {}", error.message), + } +} + +pub fn close(cx: &mut App, key: u32, force: bool) -> DesktopResult { + close_window(cx, key, force) +} + +pub fn focus(cx: &mut App, key: u32) -> DesktopResult { + let Some(entry) = cx.global::().0.get(key) else { + return Err(DesktopError::not_found(format!("window {key} is not open"))); + }; + let handle = entry.handle; + handle + .update(cx, |_root, window, _cx| window.activate_window()) + .map_err(|error| DesktopError::failed(format!("window {key} is gone: {error}")))?; + Ok(json!({"windowId": key, "focused": true})) +} + +pub(crate) fn with_window( + cx: &mut App, + key: u32, + action: impl FnOnce(&mut gpui::Window) -> Result<(), DesktopError>, +) -> DesktopResult { + let Some(entry) = cx.global::().0.get(key) else { + return Err(DesktopError::not_found(format!("window {key} is not open"))); + }; + let handle = entry.handle; + handle + .update(cx, |_root, window, _cx| action(window)) + .map_err(|error| DesktopError::failed(format!("window {key} is gone: {error}")))??; + Ok(json!({"windowId": key})) +} + +pub fn set_title(cx: &mut App, key: u32, title: String) -> DesktopResult { + if let Some(entry) = cx.global_mut::().0.get_mut(key) { + entry.title.clone_from(&title); + } + with_window(cx, key, move |window| { + window.set_window_title(&title); + Ok(()) + }) +} + +pub fn set_size(cx: &mut App, key: u32, width: f32, height: f32) -> DesktopResult { + with_window(cx, key, move |window| { + window.resize(gpui::size(px(width), px(height))); + Ok(()) + }) +} + +pub fn set_position(cx: &mut App, key: u32, x: f32, y: f32) -> DesktopResult { + if !platform_window::supports_position() { + return Err(DesktopError::unsupported( + "this session cannot position windows (Wayland has no absolute placement)", + )); + } + with_window(cx, key, move |window| { + platform_window::set_position(window, x, y).map_err(DesktopError::failed) + }) +} + +pub fn set_min_size(cx: &mut App, key: u32, width: f32, height: f32) -> DesktopResult { + with_window(cx, key, move |window| { + platform_window::set_min_size(window, width, height).map_err(DesktopError::unsupported) + }) +} + +pub fn set_max_size(cx: &mut App, key: u32, width: f32, height: f32) -> DesktopResult { + with_window(cx, key, move |window| { + platform_window::set_max_size(window, width, height).map_err(DesktopError::unsupported) + }) +} + +pub fn set_resizable(cx: &mut App, key: u32, resizable: bool) -> DesktopResult { + with_window(cx, key, move |window| { + platform_window::set_resizable(window, resizable).map_err(DesktopError::unsupported) + }) +} + +pub fn set_always_on_top(cx: &mut App, key: u32, on: bool) -> DesktopResult { + with_window(cx, key, move |window| { + platform_window::set_always_on_top(window, on).map_err(DesktopError::unsupported) + }) +} + +pub fn minimize(cx: &mut App, key: u32) -> DesktopResult { + with_window(cx, key, |window| { + window.minimize_window(); + Ok(()) + }) +} + +pub fn maximize(cx: &mut App, key: u32) -> DesktopResult { + with_window(cx, key, |window| { + if !platform_window::is_maximized(window) { + window.zoom_window(); + } + Ok(()) + }) +} + +pub fn unmaximize(cx: &mut App, key: u32) -> DesktopResult { + with_window(cx, key, |window| { + if platform_window::is_maximized(window) { + window.zoom_window(); + } + Ok(()) + }) +} + +pub fn toggle_maximize(cx: &mut App, key: u32) -> DesktopResult { + with_window(cx, key, |window| { + window.zoom_window(); + Ok(()) + }) +} + +pub fn set_fullscreen(cx: &mut App, key: u32, fullscreen: bool) -> DesktopResult { + with_window(cx, key, move |window| { + if window.is_fullscreen() != fullscreen { + window.toggle_fullscreen(); + } + Ok(()) + }) +} + +pub fn show(cx: &mut App, key: u32) -> DesktopResult { + with_window(cx, key, |window| { + platform_window::show(window).map_err(DesktopError::unsupported) + }) +} + +pub fn hide(cx: &mut App, key: u32) -> DesktopResult { + with_window(cx, key, |window| { + platform_window::hide(window).map_err(DesktopError::unsupported) + }) +} + +pub fn start_dragging(cx: &mut App, key: u32) -> DesktopResult { + with_window(cx, key, |window| { + window.start_window_move(); + Ok(()) + }) +} + +pub fn emit_window_event(cx: &App, name: &str, payload: Value) { + let Some(store) = store_from(cx) else { + return; + }; + let host = store.read(cx).host.clone(); + host.emit_device_event_json(name, &payload.to_string()); +} + +pub fn window_state(cx: &mut App, key: u32) -> DesktopResult { + let Some(entry) = cx.global::().0.get(key) else { + return Err(DesktopError::not_found(format!("window {key} is not open"))); + }; + let handle = entry.handle; + handle + .update(cx, |_root, window, _cx| { + let bounds = window.bounds(); + json!({ + "windowId": key, + "bounds": { + "x": f32::from(bounds.origin.x), + "y": f32::from(bounds.origin.y), + "width": f32::from(bounds.size.width), + "height": f32::from(bounds.size.height), + }, + "isMaximized": platform_window::is_maximized(window), + "isFullscreen": window.is_fullscreen(), + "isFocused": window.is_window_active(), + "isMinimized": platform_window::is_minimized(window), + "scaleFactor": window.scale_factor(), + }) + }) + .map_err(|error| DesktopError::failed(format!("window {key} is gone: {error}"))) +} + +/// The OS's own frame for a window, in physical pixels, for the test driver: +/// `{left, top, right, bottom, visible}`, or `null` where the platform cannot say. +#[cfg(feature = "test-driver")] +pub fn native_frame_json(cx: &mut App, key: u32) -> Value { + #[cfg(target_os = "windows")] + { + let Some(entry) = cx.global::().0.get(key) else { + return Value::Null; + }; + let handle = entry.handle; + return handle + .update(cx, |_root, window, _cx| { + platform_window::native_frame(window).map(|(left, top, right, bottom, visible)| { + json!({ + "left": left, + "top": top, + "right": right, + "bottom": bottom, + "visible": visible, + }) + }) + }) + .ok() + .flatten() + .unwrap_or(Value::Null); + } + #[cfg(not(target_os = "windows"))] + { + let _ = (cx, key); + Value::Null + } +} + +pub fn list_windows(cx: &App) -> DesktopResult { + let windows = &cx.global::().0; + let list: Vec = windows + .keys() + .into_iter() + .filter_map(|key| { + let entry = windows.get(key)?; + Some(json!({ + "windowId": key, + "component": entry.component, + "role": match entry.role { + WindowRole::Main => "main", + WindowRole::Secondary => "secondary", + WindowRole::TrayPopover => "trayPopover", + }, + })) + }) + .collect(); + Ok(json!({"windows": list})) +} + +fn install_bounds_observer( + window: &mut gpui::Window, + cx: &mut gpui::Context, +) -> Subscription { + let last = std::rc::Rc::new(std::cell::Cell::new(None::<(f32, f32, f32, f32)>)); + cx.observe_window_bounds(window, move |_root, window, cx| { + let size = window.viewport_size(); + let bounds = window.bounds(); + let (width, height) = (f32::from(size.width), f32::from(size.height)); + let (x, y) = (f32::from(bounds.origin.x), f32::from(bounds.origin.y)); + let scale = window.scale_factor(); + if last.get() == Some((x, y, width, height)) { + return; + } + last.set(Some((x, y, width, height))); + let handle: AnyWindowHandle = window.window_handle().into(); + let Some(store) = cx.try_global::() else { + return; + }; + let store = store.0.clone(); + store.update(cx, |store, _cx| { + store.resize_window(handle, width, height, scale) + }); + let frame_hook = store.read(cx).frame_hook.clone(); + frame_hook.arm(window); + if let Some(key) = window_key_for_handle(cx, handle) { + let host = store.read(cx).host.clone(); + host.emit_device_event_json( + "gpuiDesktop:windowResize", + &json!({"windowId": key, "width": width, "height": height}).to_string(), + ); + host.emit_device_event_json( + "gpuiDesktop:windowMove", + &json!({"windowId": key, "x": x, "y": y}).to_string(), + ); + } + }) +} + +pub(crate) fn window_key_for_handle(cx: &gpui::App, handle: AnyWindowHandle) -> Option { + let windows = cx.try_global::()?; + windows + .0 + .keys() + .into_iter() + .find(|key| windows.0.get(*key).map(|entry| entry.handle) == Some(handle)) +} + +/// Sends `gpuiDesktop:windowStateChange` when the window is maximized, restored, +/// minimized or made fullscreen. +/// +/// `DesktopWindow`'s `stateChange` listeners wait on it, and so does a caption +/// button's maximize glyph, which has to turn into the restore glyph however the +/// window got maximized — a double click on the title bar, Win+Up, a snap — not +/// only when the button itself was pressed. Every one of those resizes the +/// window, so the bounds observer is where the change is seen. +pub(crate) fn install_state_observer( + window: &mut gpui::Window, + cx: &mut gpui::Context, +) -> Subscription { + let state_of = |window: &gpui::Window| { + ( + platform_window::is_maximized(window), + platform_window::is_minimized(window), + window.is_fullscreen(), + ) + }; + let mut last = state_of(window); + cx.observe_window_bounds(window, move |_root, window, cx| { + let state = state_of(window); + if state == last { + return; + } + last = state; + let handle: AnyWindowHandle = window.window_handle().into(); + let Some(key) = window_key_for_handle(cx, handle) else { + return; + }; + let Some(store) = cx.try_global::() else { + return; + }; + let host = store.0.read(cx).host.clone(); + host.emit_device_event_json( + "gpuiDesktop:windowStateChange", + &json!({ + "windowId": key, + "isMaximized": state.0, + "isMinimized": state.1, + "isFullscreen": state.2, + }) + .to_string(), + ); + }) +} + +fn install_activation_observer( + window: &mut gpui::Window, + cx: &mut gpui::Context, + key: u32, +) -> Subscription { + let mut last = window.is_window_active(); + cx.observe_window_activation(window, move |_root, window, cx| { + let active = window.is_window_active(); + if active == last { + return; + } + last = active; + let Some(store) = cx.try_global::() else { + return; + }; + let host = store.0.read(cx).host.clone(); + let name = if active { + "gpuiDesktop:windowFocus" + } else { + "gpuiDesktop:windowBlur" + }; + host.emit_device_event_json(name, &json!({"windowId": key}).to_string()); + if !active { + let is_popover = cx + .try_global::() + .is_some_and(|tray| { + let mut tray = tray.0.borrow_mut(); + if tray.popover == Some(key) { + tray.popover = None; + tray.popover_dismissed_at = Some(std::time::Instant::now()); + true + } else { + false + } + }); + if is_popover { + // Deferred: this observer runs inside the activation change's + // window update, and closing a window from there re-enters it, + // which GPUI reports as "RefCell already borrowed". + cx.defer(move |cx| { + let _ = close_window(cx, key, true); + }); + } + } + }) +} + +fn install_close_hook( + window: &mut gpui::Window, + cx: &mut gpui::Context, + key: u32, +) { + let last_request: std::rc::Rc>> = + std::rc::Rc::new(std::cell::Cell::new(None)); + window.on_window_should_close(cx, move |window, cx| { + let intercept = cx + .try_global::() + .and_then(|windows| windows.0.get(key).map(|entry| entry.intercept_close)) + .unwrap_or(false); + // A second close request within three seconds closes the window even if + // JS never answered, so a hung or mid-reload app can always be quit. + let unanswered = intercept + && last_request.get().is_some_and(|previous| { + std::time::Instant::now().duration_since(previous) + < std::time::Duration::from_secs(3) + }); + // A tray that asked for it keeps the last window: hidden, not destroyed, so + // that opening the app again shows it with its state. JavaScript's own + // veto (`closeRequested`) comes first, below, and decides for itself. + if !intercept && hides_on_close(cx, key) { + if let Err(error) = platform_window::hide(window) { + log::warn!("[desktop] could not hide window {key}: {error}"); + } else { + log::info!("[desktop] hid window {key} instead of closing it"); + return false; + } + } + if !intercept || unanswered { + if unanswered { + log::warn!("[desktop] window {key} close was not answered by JS; closing"); + } + // The OS is closing it: run the same teardown as `close_window`, + // without asking GPUI to remove a window it is already removing. + let entry = if cx.has_global::() { + cx.global_mut::().0.remove(key) + } else { + None + }; + if let Some(entry) = entry { + finish_close(cx, key, entry, false); + } + return true; + } + last_request.set(Some(std::time::Instant::now())); + emit_close_requested(cx, window, key); + false + }); +} + +/// Whether closing window `key` should hide it instead. +fn hides_on_close(cx: &App, key: u32) -> bool { + let Some(windows) = cx.try_global::() else { + return false; + }; + let Some(entry) = windows.0.get(key) else { + return false; + }; + let other_windows_open = windows + .0 + .by_key + .values() + .any(|other| other.key != key && other.role != WindowRole::TrayPopover); + should_hide_on_close( + crate::desktop::tray::hides_window_on_close(cx), + quit_on_last_window_closed(cx), + entry.role, + other_windows_open, + ) +} + +/// The decision, apart from where its inputs come from. +/// +/// Only the last window the user can close is hidden: closing one of several +/// closes it, as it always did. A popover is never hidden — it is closed so each +/// show opens a fresh one. And an app that quits when its last window closes is +/// not asking for a window to be kept, whatever else it set. +fn should_hide_on_close( + hide_requested: bool, + quits_on_last_window: bool, + role: WindowRole, + other_windows_open: bool, +) -> bool { + hide_requested + && !quits_on_last_window + && role != WindowRole::TrayPopover + && !other_windows_open +} + +fn emit_close_requested(cx: &mut App, window: &mut gpui::Window, key: u32) { + let Some(store) = cx.try_global::() else { + return; + }; + let host = store.0.read(cx).host.clone(); + host.emit_device_event_json( + "gpuiDesktop:closeRequested", + &json!({"windowId": key}).to_string(), + ); + store.0.read(cx).frame_hook.arm(window); +} + +pub fn window_id_for_surface(cx: &App, surface_id: i32) -> Option { + cx.try_global::().and_then(|windows| { + windows + .0 + .entry_for_surface(surface_id) + .map(|entry| entry.key) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn keys_and_surfaces_are_allocated_without_collisions() { + let mut windows = DesktopWindows::new(); + assert_eq!(windows.alloc_key(), 2); + assert_eq!(windows.alloc_key(), 3); + assert_eq!(windows.alloc_surface(), 3); + assert_eq!(windows.alloc_surface(), 5); + assert_eq!(windows.alloc_surface(), 7); + assert!(windows.keys().is_empty()); + assert!(windows.get(1).is_none()); + assert!(windows.entry_for_surface(1).is_none()); + } + + #[test] + fn the_last_window_is_hidden_when_the_tray_asked_for_it() { + assert!(should_hide_on_close(true, false, WindowRole::Main, false)); + assert!(should_hide_on_close( + true, + false, + WindowRole::Secondary, + false + )); + } + + #[test] + fn nothing_is_hidden_unless_it_was_asked_for() { + assert!(!should_hide_on_close(false, false, WindowRole::Main, false)); + } + + #[test] + fn closing_one_of_several_windows_still_closes_it() { + assert!(!should_hide_on_close(true, false, WindowRole::Main, true)); + } + + #[test] + fn a_popover_is_closed_never_hidden() { + assert!(!should_hide_on_close( + true, + false, + WindowRole::TrayPopover, + false + )); + } + + #[test] + fn an_app_that_quits_with_its_last_window_keeps_none() { + assert!(!should_hide_on_close(true, true, WindowRole::Main, false)); + } +} diff --git a/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/events.rs b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/events.rs new file mode 100644 index 0000000000..6a3afade12 --- /dev/null +++ b/vendor/react-native-gpui/packages/core/ReactGpui/rn-gpui/src/events.rs @@ -0,0 +1,885 @@ +//! Pointer, touch and hover events. +//! +//! `Pressable` consumes touch events (`ResponderEventPlugin`); pointer events +//! are sent alongside. Hover enter/leave and `pointerEvents` are implemented +//! here: RN's `PointerEventsProcessor` only runs for typed `PointerEvent` +//! payloads and never reads `pointerEvents`. + +use crate::node_view::RNNodeView; +use crate::store::{store_from, StoreGlobal}; +use gpui::{point, px, App, Pixels, Point, WeakEntity, Window}; +use serde_json::{json, Map, Value}; + +const POINTER_ID: i32 = 1; + +pub const POINTER_EVENTS_AUTO: u8 = 0; +pub const POINTER_EVENTS_NONE: u8 = 1; +pub const POINTER_EVENTS_BOX_NONE: u8 = 2; +pub const POINTER_EVENTS_BOX_ONLY: u8 = 3; + +thread_local! { + /// Whether the press (or release) being dispatched belongs to the window: + /// it landed in a title bar's drag area or on a caption button. On Windows + /// the OS only moves the window, maximizes it on a double click or presses + /// the button when no node consumes the press — and a press bubbles from + /// the node under the pointer to every ancestor, each of which would + /// otherwise start a touch of its own and consume it. + static WINDOW_OWNS_PRESS: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +/// Clears the mark before a press or release reaches any node: the surface's +/// root calls it in the capture phase, which runs ahead of every node's own +/// listener. +pub(crate) fn begin_press_dispatch() { + WINDOW_OWNS_PRESS.with(|owned| owned.set(false)); +} + +/// Marks the press as the window's. The node under the pointer is the first to +/// see a press in the bubble phase, so its ancestors read this afterwards. +pub(crate) fn hand_press_to_window() { + WINDOW_OWNS_PRESS.with(|owned| owned.set(true)); +} + +/// Whether a node nearer the pointer already handed this press to the window. +pub(crate) fn press_belongs_to_window() -> bool { + WINDOW_OWNS_PRESS.with(std::cell::Cell::get) +} + +#[derive(Clone, Copy, Debug)] +pub enum Phase { + Start, + Move, + End, + Cancel, +} + +/// Process-relative, like the typed `TouchEventEmitter` path: RN only uses +/// differences. +pub fn now_ms() -> f64 { + use std::sync::OnceLock; + static START: OnceLock = OnceLock::new(); + START + .get_or_init(std::time::Instant::now) + .elapsed() + .as_secs_f64() + * 1000.0 +} + +/// Precomputed because the node whose handler is running is being updated by +/// GPUI, and reading it from the entity map panics. +#[derive(Clone, Debug)] +pub struct Target { + pub tag: i32, + pub origin: Point, + scale_x: f32, + scale_y: f32, + /// The target and its ancestors, innermost first: RN's hover path + /// (`PointerHoverTracker::getEventPathTargets`), the surface root excluded. + pub chain: Vec, +} + +pub fn active_touch_target(cx: &App) -> Option { + store_from(cx).and_then(|store| store.read(cx).active_touch_target.clone()) +} + +impl Target { + fn local_position(&self, position: Point) -> Point { + point( + px(f32::from(position.x - self.origin.x) / self.scale_x.max(0.0001)), + px(f32::from(position.y - self.origin.y) / self.scale_y.max(0.0001)), + ) + } +} + +#[cfg(test)] +mod transform_tests { + use super::Target; + use gpui::{point, px}; + + #[test] + fn pointer_location_is_reported_in_untransformed_target_coordinates() { + let target = Target { + tag: 7, + origin: point(px(30.0), px(40.0)), + scale_x: 2.0, + scale_y: 0.5, + chain: vec![7], + }; + let local = target.local_position(point(px(70.0), px(60.0))); + assert_eq!(f32::from(local.x), 20.0); + assert_eq!(f32::from(local.y), 40.0); + } +} + +struct Step { + tag: i32, + pointer_events: u8, + entity: Option>, + parent: Option>, +} + +/// Mirrors RN's native `hitTest`: `auto` is a candidate (the deepest wins), +/// `box-none` is never a target but its children are, `box-only` overrides any +/// deeper candidate, `none` kills the whole subtree. +/// The node that owns the context menu for a press on `hit_node`. +/// +/// Right-clicking a `Text` inside a `View` with `onContextMenu` must open that +/// view's menu. GPUI bubbles mouse events from the innermost hitbox outwards, so +/// relying on bubbling means every node between the two needs a listener — and a +/// node that takes pointer input stops the press before it reaches the owner. +/// That is what made a right click on the text stop working. +/// +/// So the innermost node that handles the press walks its ancestors instead. The +/// walk follows the same `pointerEvents` rules as `target`: `none` ends it, and +/// `box-only` is a candidate but its ancestors are not. +/// +/// The result is the nearest ancestor-or-self that asked for a context menu — +/// either a `View` with the prop or an extension component whose `InputSpec` +/// does. `None` means nothing in the chain wants one, and the press is only a +/// pointer event. +pub fn context_menu_owner(cx: &App, hit_node: &RNNodeView) -> Option { + let mut tag = hit_node.tag; + let mut wants = node_wants_context_menu(cx, hit_node); + let mut pointer_events = hit_node.view.pointer_events; + let mut parent = hit_node.parent.clone(); + // `None` for the hit node itself, which the caller already has. + let mut entity: Option> = None; + loop { + if pointer_events == POINTER_EVENTS_NONE { + return None; + } + if wants { + return Some(ContextMenuOwner { tag, entity }); + } + // `box-only` claims the press for itself, so the search ends with it + // whether or not it wants a menu. + if pointer_events == POINTER_EVENTS_BOX_ONLY { + return None; + } + let Some(next) = parent.clone() else { + return None; + }; + let Some(node) = next.upgrade() else { + return None; + }; + let node = node.read(cx); + tag = node.tag; + wants = node_wants_context_menu(cx, node); + pointer_events = node.view.pointer_events; + parent = node.parent.clone(); + entity = Some(next); + } +} + +/// Whether this node wants a context menu: the View prop, or an extension +/// component whose `InputSpec` asks for one. +fn node_wants_context_menu(cx: &App, node: &RNNodeView) -> bool { + if node.view.on_context_menu { + return true; + } + let Some(renderer) = crate::extensions::component_renderer(&node.component) else { + return false; + }; + let component_node = node.component_node(cx); + renderer.input(&component_node).context_menu +} + +/// Who owns a context menu for a press. +#[derive(Clone)] +pub struct ContextMenuOwner { + pub tag: i32, + /// The owning node, or `None` when it is the hit node itself. + pub entity: Option>, +} + +pub fn target(cx: &App, hit_node: &RNNodeView) -> Option { + let mut steps = vec![Step { + tag: hit_node.tag, + pointer_events: hit_node.view.pointer_events, + entity: None, + parent: hit_node.parent.clone(), + }]; + loop { + let Some(ancestor) = steps.last().and_then(|step| step.parent.clone()) else { + break; + }; + let Some(node) = ancestor.upgrade() else { + break; + }; + let (tag, pointer_events, parent) = { + let node = node.read(cx); + (node.tag, node.view.pointer_events, node.parent.clone()) + }; + steps.push(Step { + tag, + pointer_events, + entity: Some(ancestor), + parent, + }); + } + let mut chosen: Option = None; + for (index, step) in steps.iter().enumerate() { + match step.pointer_events { + POINTER_EVENTS_NONE => return None, + POINTER_EVENTS_AUTO => { + if chosen.is_none() { + chosen = Some(index); + } + } + POINTER_EVENTS_BOX_NONE => {} + POINTER_EVENTS_BOX_ONLY => chosen = Some(index), + other => { + log::warn!("[events] unknown pointerEvents {other}; treating it as auto"); + if chosen.is_none() { + chosen = Some(index); + } + } + } + } + let index = chosen?; + let (origin, scale_x, scale_y) = if index == 0 { + let (scale_x, scale_y) = hit_node.cumulative_scale(cx); + (hit_node.absolute_origin(cx), scale_x, scale_y) + } else { + let node = steps[index].entity.as_ref()?.upgrade()?; + let node = node.read(cx); + let (scale_x, scale_y) = node.cumulative_scale(cx); + (node.absolute_origin(cx), scale_x, scale_y) + }; + let chain = steps[index..].iter().map(|step| step.tag).collect(); + Some(Target { + tag: steps[index].tag, + origin, + scale_x, + scale_y, + chain, + }) +} + +/// One touch object: exactly the keys `setTouchPayloadOnObject` writes +/// (`ReactCommon/react/renderer/components/view/BaseTouch.cpp`). +fn touch_json(tag: i32, page: Point, location: Point, timestamp: f64) -> Value { + let (x, y) = (f32::from(page.x), f32::from(page.y)); + json!({ + "identifier": POINTER_ID, + "target": tag, + "pageX": x, + "pageY": y, + "locationX": f32::from(location.x), + "locationY": f32::from(location.y), + "screenX": x, + "screenY": y, + "timestamp": timestamp, + "force": 0.0, + }) +} + +/// `touches` must be empty for `topTouchEnd`: `ResponderTouchHistoryStore` +/// reads its length as the number of active touches. +fn touch_payload(tag: i32, page: Point, location: Point, active: bool) -> Value { + let touch = touch_json(tag, page, location, now_ms()); + let mut payload = touch.as_object().cloned().unwrap_or_else(Map::new); + let list = |touch: &Value| Value::Array(if active { vec![touch.clone()] } else { vec![] }); + payload.insert("touches".into(), list(&touch)); + payload.insert("changedTouches".into(), Value::Array(vec![touch.clone()])); + payload.insert("targetTouches".into(), list(&touch)); + Value::Object(payload) +} + +/// Delivers only the DOM pointer events for a button, with no touch or responder +/// side effects. +/// +/// This is the path a middle or right press takes. `send` also dispatches +/// `topTouchStart` and feeds RN's responder system, which would make a +/// `Pressable` react to a right click; a non-primary button must not do that. +pub fn send_pointer( + window: &mut Window, + cx: &mut App, + target: &Target, + position: Point, + phase: Phase, + button: Option