basalt-core 0.0.0-rc5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/cli/basalt.d.ts.map +1 -1
  2. package/dist/cli/basalt.js +18 -1
  3. package/dist/cli/basalt.js.map +1 -1
  4. package/dist/cli/bundle-cli.js +0 -0
  5. package/dist/cli/desktop.d.ts.map +1 -1
  6. package/dist/cli/desktop.js +53 -4
  7. package/dist/cli/desktop.js.map +1 -1
  8. package/dist/cli/packageApp.d.ts.map +1 -1
  9. package/dist/cli/packageApp.js +7 -1
  10. package/dist/cli/packageApp.js.map +1 -1
  11. package/dist/metro-config.d.ts.map +1 -1
  12. package/dist/metro-config.js +53 -5
  13. package/dist/metro-config.js.map +1 -1
  14. package/dist/src/KeyHandler.d.ts +1 -1
  15. package/dist/src/KeyHandler.d.ts.map +1 -1
  16. package/dist/src/KeyHandler.js.map +1 -1
  17. package/dist/src/index.d.ts +5 -0
  18. package/dist/src/index.d.ts.map +1 -1
  19. package/dist/src/overrides/Alert.d.ts +1 -1
  20. package/dist/src/overrides/Alert.js +1 -1
  21. package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
  22. package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
  23. package/dist/src/overrides/BaseViewConfig.js +21 -3
  24. package/dist/src/overrides/BaseViewConfig.js.map +1 -1
  25. package/dist/src/overrides/Button.d.ts +81 -0
  26. package/dist/src/overrides/Button.d.ts.map +1 -0
  27. package/dist/src/overrides/Button.js +109 -0
  28. package/dist/src/overrides/Button.js.map +1 -0
  29. package/dist/src/overrides/ScrollView.d.ts +25 -2
  30. package/dist/src/overrides/ScrollView.d.ts.map +1 -1
  31. package/dist/src/overrides/ScrollView.js +64 -7
  32. package/dist/src/overrides/ScrollView.js.map +1 -1
  33. package/dist/src/overrides/Settings.d.ts +81 -0
  34. package/dist/src/overrides/Settings.d.ts.map +1 -0
  35. package/dist/src/overrides/Settings.js +120 -0
  36. package/dist/src/overrides/Settings.js.map +1 -0
  37. package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
  38. package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
  39. package/dist/src/overrides/processDecelerationRate.js +31 -0
  40. package/dist/src/overrides/processDecelerationRate.js.map +1 -0
  41. package/dist/src/useContextMenu.d.ts +57 -6
  42. package/dist/src/useContextMenu.d.ts.map +1 -1
  43. package/dist/src/useContextMenu.js +34 -1
  44. package/dist/src/useContextMenu.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/native/CMakeLists.txt +85 -1
  47. package/native/cmake/Skia.cmake +29 -6
  48. package/native/core/ActionSheet.cpp +75 -0
  49. package/native/core/ActionSheet.h +90 -0
  50. package/native/core/AppStateHost.cpp +76 -0
  51. package/native/core/AppStateHost.h +68 -0
  52. package/native/core/BackgroundLayers.h +282 -0
  53. package/native/core/BlendModes.h +67 -0
  54. package/native/core/CoreModules.cpp +380 -11
  55. package/native/core/CoreModules.h +140 -4
  56. package/native/core/CrashHandler.cpp +246 -0
  57. package/native/core/CrashHandler.h +65 -0
  58. package/native/core/CursorNames.h +110 -0
  59. package/native/core/DevMenu.cpp +6 -5
  60. package/native/core/DeviceMetrics.cpp +87 -0
  61. package/native/core/DeviceMetrics.h +80 -0
  62. package/native/core/Filters.h +336 -0
  63. package/native/core/FontFitting.h +183 -0
  64. package/native/core/FontScaling.h +153 -0
  65. package/native/core/FontVariants.h +148 -0
  66. package/native/core/Gradients.h +523 -0
  67. package/native/core/ImageAnimation.h +109 -0
  68. package/native/core/ImageBytes.cpp +28 -1
  69. package/native/core/ImageBytes.h +17 -1
  70. package/native/core/ImageProgress.h +85 -0
  71. package/native/core/LabelRegistry.h +189 -0
  72. package/native/core/LegacyShadow.h +90 -0
  73. package/native/core/LiveRegions.h +114 -0
  74. package/native/core/MenuModule.cpp +35 -4
  75. package/native/core/MountingWalk.h +125 -2
  76. package/native/core/PlatformServices.h +187 -3
  77. package/native/core/SafeAreaComponent.cpp +115 -0
  78. package/native/core/SafeAreaComponent.h +105 -0
  79. package/native/core/ScreensComponent.cpp +201 -0
  80. package/native/core/ScreensComponent.h +216 -0
  81. package/native/core/ScrollBounds.cpp +8 -0
  82. package/native/core/ScrollBounds.h +14 -0
  83. package/native/core/ScrollIndicator.h +67 -0
  84. package/native/core/ScrollMomentum.cpp +24 -0
  85. package/native/core/ScrollMomentum.h +14 -0
  86. package/native/core/ScrollSnap.cpp +121 -12
  87. package/native/core/ScrollSnap.h +45 -0
  88. package/native/core/ScrollVisiblePosition.cpp +68 -0
  89. package/native/core/ScrollVisiblePosition.h +101 -0
  90. package/native/core/SettingsStore.cpp +199 -0
  91. package/native/core/SettingsStore.h +127 -0
  92. package/native/core/ShadowShape.h +38 -0
  93. package/native/core/SkiaViewComponent.cpp +11 -0
  94. package/native/core/SkiaViewComponent.h +50 -0
  95. package/native/core/SystemLocale.cpp +103 -0
  96. package/native/core/SystemLocale.h +86 -0
  97. package/native/core/TestDialog.cpp +45 -6
  98. package/native/core/TestDialog.h +15 -0
  99. package/native/core/TextAlignments.h +240 -0
  100. package/native/core/TextChecking.h +133 -0
  101. package/native/core/TextColors.h +83 -0
  102. package/native/core/TextDecorations.h +101 -0
  103. package/native/core/TextDirection.cpp +242 -0
  104. package/native/core/TextDirection.h +88 -0
  105. package/native/core/TextHighlight.h +30 -0
  106. package/native/core/TextSelection.cpp +97 -0
  107. package/native/core/TextSelection.h +161 -0
  108. package/native/core/TextShadows.h +96 -0
  109. package/native/core/TextVerticalAlign.h +33 -0
  110. package/native/core/UIManagerAccess.cpp +36 -0
  111. package/native/core/UIManagerAccess.h +28 -0
  112. package/native/core/WindowBoundsCache.cpp +58 -1
  113. package/native/core/WindowControl.h +24 -0
  114. package/native/core/WritingDirections.h +52 -0
  115. package/native/core/portability_probe.cpp +7 -0
  116. package/native/tests/RenamedFontFile.h +161 -0
  117. package/native/tests/TestHarness.cpp +18 -0
  118. package/native/tests/test_action_sheet.cpp +157 -0
  119. package/native/tests/test_app_state.cpp +60 -0
  120. package/native/tests/test_background_layers.cpp +281 -0
  121. package/native/tests/test_blend_modes.cpp +79 -0
  122. package/native/tests/test_blobs.cpp +1 -1
  123. package/native/tests/test_device_metrics.cpp +126 -0
  124. package/native/tests/test_filters.cpp +271 -0
  125. package/native/tests/test_font_fitting.cpp +172 -0
  126. package/native/tests/test_font_scaling.cpp +127 -0
  127. package/native/tests/test_font_variants.cpp +84 -0
  128. package/native/tests/test_gradients.cpp +390 -0
  129. package/native/tests/test_image_animation.cpp +105 -0
  130. package/native/tests/test_image_progress.cpp +94 -0
  131. package/native/tests/test_label_registry.cpp +262 -0
  132. package/native/tests/test_legacy_shadow.cpp +165 -0
  133. package/native/tests/test_live_regions.cpp +108 -0
  134. package/native/tests/test_menu_entries.cpp +192 -0
  135. package/native/tests/test_momentum.cpp +45 -0
  136. package/native/tests/test_scroll_bounds.cpp +42 -0
  137. package/native/tests/test_scroll_indicator.cpp +61 -0
  138. package/native/tests/test_scroll_snap.cpp +129 -0
  139. package/native/tests/test_scroll_visible_position.cpp +121 -0
  140. package/native/tests/test_settings_store.cpp +257 -0
  141. package/native/tests/test_shadow_shape.cpp +47 -0
  142. package/native/tests/test_system_locale.cpp +36 -0
  143. package/native/tests/test_text_alignment.cpp +299 -0
  144. package/native/tests/test_text_checking.cpp +31 -0
  145. package/native/tests/test_text_colors.cpp +92 -0
  146. package/native/tests/test_text_decorations.cpp +100 -0
  147. package/native/tests/test_text_selection.cpp +168 -0
  148. package/native/tests/test_text_shadows.cpp +132 -0
  149. package/package.json +4 -2
@@ -1,9 +1,15 @@
1
1
  #include "CoreModules.h"
2
2
 
3
+ #include "DeviceMetrics.h"
4
+ #include "SystemLocale.h"
5
+
3
6
  #include "ShareFallback.h"
4
7
  #include "TestDialog.h"
8
+ #include "UIManagerAccess.h"
5
9
 
10
+ #include <jsi/JSIDynamic.h>
6
11
  #include <react/bridging/Promise.h>
12
+ #include <react/renderer/core/LayoutableShadowNode.h>
7
13
 
8
14
  #include <glog/logging.h>
9
15
 
@@ -236,25 +242,36 @@ I18nState &i18n() {
236
242
 
237
243
  Object DesktopI18nManagerModule::getConstants(Runtime &rt) {
238
244
  I18nState &state = i18n();
239
- const std::lock_guard<std::mutex> lock(state.mutex);
245
+ const SystemLocale locale = lastKnownSystemLocale();
240
246
 
241
247
  Object constants(rt);
242
- // `isRTL` is what the layout actually is. Only `forceRTL` can make it true
243
- // here: deciding it from the system locale would need a locale database this
244
- // does not have, and guessing wrong flips an app's entire layout.
245
- constants.setProperty(rt, "isRTL", state.forceRTL);
246
- constants.setProperty(rt, "doLeftAndRightSwapInRTL", state.swapLeftAndRight);
247
- // Empty rather than a guess. React Native's JavaScript treats it as opaque
248
- // and only apps read it.
249
- constants.setProperty(rt, "localeIdentifier", String::createFromUtf8(rt, ""));
248
+ // `isRTL` is what the layout actually is, and since 2026-10-10 that includes
249
+ // the desktop's own language: every platform publishes a reading direction,
250
+ // which is a different question from parsing a locale name. See
251
+ // core/SystemLocale.h, which also decides the precedence between this and
252
+ // the two calls below.
253
+ constants.setProperty(rt, "isRTL", layoutIsRightToLeft());
254
+ {
255
+ const std::lock_guard<std::mutex> lock(state.mutex);
256
+ constants.setProperty(rt, "doLeftAndRightSwapInRTL", state.swapLeftAndRight);
257
+ }
258
+ constants.setProperty(rt,
259
+ "localeIdentifier",
260
+ String::createFromUtf8(rt, locale.identifier));
250
261
  return constants;
251
262
  }
252
263
 
253
264
  void DesktopI18nManagerModule::allowRTL(Runtime &rt, bool allow) {
254
265
  (void)rt;
255
266
  I18nState &state = i18n();
256
- const std::lock_guard<std::mutex> lock(state.mutex);
257
- state.allowRTL = allow;
267
+ {
268
+ const std::lock_guard<std::mutex> lock(state.mutex);
269
+ state.allowRTL = allow;
270
+ }
271
+ // And the copy the layout reads, which is core's: a host building layout
272
+ // constraints cannot lock this module's state, and the two must not be able
273
+ // to disagree about which way the app reads.
274
+ setAllowRTL(allow);
258
275
  }
259
276
 
260
277
  void DesktopI18nManagerModule::forceRTL(Runtime &rt, bool force) {
@@ -262,6 +279,7 @@ void DesktopI18nManagerModule::forceRTL(Runtime &rt, bool force) {
262
279
  I18nState &state = i18n();
263
280
  const std::lock_guard<std::mutex> lock(state.mutex);
264
281
  state.forceRTL = force;
282
+ setForceRTL(force);
265
283
  // Deliberately not restarting or relaying out. On iOS and Android this takes
266
284
  // effect after a reload, and React Native's own documentation says so -- an
267
285
  // app that flipped direction mid-session would be a surprise rather than a
@@ -589,4 +607,355 @@ Value DesktopShareModule::share(Runtime &rt, Object content, std::optional<Strin
589
607
  return Value(rt, facebook::react::bridging::toJs(rt, *promise, jsInvoker_));
590
608
  }
591
609
 
610
+ // ---------------------------------------------------------------------------
611
+ // AppState
612
+ // ---------------------------------------------------------------------------
613
+
614
+ DesktopAppStateModule::DesktopAppStateModule(
615
+ std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
616
+ : facebook::react::NativeAppStateCxxSpec<DesktopAppStateModule>(std::move(jsInvoker)) {
617
+ setAppStateListener([this](AppStateKind state) {
618
+ const std::string name = appStateName(state);
619
+ emitDeviceEvent("appStateDidChange",
620
+ [name](Runtime &runtime, std::vector<Value> &args) {
621
+ Object payload(runtime);
622
+ payload.setProperty(runtime, "app_state",
623
+ String::createFromUtf8(runtime, name));
624
+ args.emplace_back(std::move(payload));
625
+ });
626
+ });
627
+ }
628
+
629
+ DesktopAppStateModule::~DesktopAppStateModule() {
630
+ // The listener captured `this`; a surface torn down and started again builds
631
+ // a new module.
632
+ setAppStateListener(nullptr);
633
+ }
634
+
635
+ facebook::react::AppStateConstants DesktopAppStateModule::getConstants(Runtime & /*rt*/) {
636
+ facebook::react::AppStateConstants constants;
637
+ constants.initialAppState = appStateName(currentAppState());
638
+ return constants;
639
+ }
640
+
641
+ void DesktopAppStateModule::getCurrentAppState(
642
+ Runtime & /*rt*/,
643
+ const facebook::react::AsyncCallback<facebook::react::AppState> &success,
644
+ facebook::jsi::Function /*error*/) {
645
+ // Answered rather than dropped, which is the whole of what upstream's does:
646
+ // `AppState.js` calls this once at startup to populate `currentState`, and a
647
+ // callback that is never called leaves it as whatever `getConstants` said.
648
+ facebook::react::AppState state;
649
+ state.app_state = appStateName(currentAppState());
650
+ success(std::move(state));
651
+ }
652
+
653
+ void DesktopAppStateModule::addListener(Runtime & /*rt*/, const std::string & /*eventName*/) {}
654
+
655
+ void DesktopAppStateModule::removeListeners(Runtime & /*rt*/, double /*count*/) {}
656
+
657
+ // ---------------------------------------------------------------------------
658
+ // Dimensions and PixelRatio
659
+ // ---------------------------------------------------------------------------
660
+
661
+ DesktopDeviceInfoModule::DesktopDeviceInfoModule(
662
+ std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
663
+ : facebook::react::NativeDeviceInfoCxxSpec<DesktopDeviceInfoModule>(std::move(jsInvoker)) {
664
+ // `didUpdateDimensions`, which `Dimensions.js` subscribes to before it ever
665
+ // asks for the constants. The listener is core's second slot on the window
666
+ // bounds signal; see WindowControl.h for why there are two.
667
+ setDimensionsListener([this]() {
668
+ const basalt::DeviceMetrics metrics = currentDeviceMetrics();
669
+ if (metrics == lastSent_) {
670
+ return;
671
+ }
672
+ lastSent_ = metrics;
673
+ emitDeviceEvent("didUpdateDimensions",
674
+ [metrics](Runtime &runtime, std::vector<Value> &args) {
675
+ Object payload(runtime);
676
+ const auto describe = [&runtime](const basalt::DisplayMetrics &one) {
677
+ Object out(runtime);
678
+ out.setProperty(runtime, "width", one.width);
679
+ out.setProperty(runtime, "height", one.height);
680
+ out.setProperty(runtime, "scale", one.scale);
681
+ out.setProperty(runtime, "fontScale", one.fontScale);
682
+ return out;
683
+ };
684
+ payload.setProperty(runtime, "window", describe(metrics.window));
685
+ payload.setProperty(runtime, "screen", describe(metrics.screen));
686
+ args.emplace_back(std::move(payload));
687
+ });
688
+ });
689
+ }
690
+
691
+ DesktopDeviceInfoModule::~DesktopDeviceInfoModule() {
692
+ // The listener captured `this`. A surface torn down and started again builds
693
+ // a new module, and the old one must stop being called first.
694
+ setDimensionsListener(nullptr);
695
+ }
696
+
697
+ facebook::react::DeviceInfoConstants DesktopDeviceInfoModule::getConstants(Runtime & /*rt*/) {
698
+ const basalt::DeviceMetrics metrics = currentDeviceMetrics();
699
+ // What JavaScript now has, so the first resize after this does not re-send
700
+ // the same numbers.
701
+ lastSent_ = metrics;
702
+ // Every field, by assignment rather than by designated initialiser.
703
+ //
704
+ // Two of each struct's fields are Android's physical-pixel payloads and
705
+ // iOS's notch flag, which this platform has nothing to say about -- and
706
+ // naming only the fields that matter is what `-Wmissing-field-initializers`
707
+ // is for. Which is not a style point: the *generated* `DisplayMetrics`
708
+ // declares `fontScale` with no default member initialiser, so a partial
709
+ // initialisation leaves it as whatever was on the stack, which is the bug
710
+ // upstream's own module has. All four are filled here.
711
+ const auto describe = [](const basalt::DisplayMetrics &one) {
712
+ return facebook::react::DisplayMetrics{one.width, one.height, one.scale, one.fontScale};
713
+ };
714
+
715
+ facebook::react::DeviceInfoConstants constants;
716
+ constants.Dimensions.window = describe(metrics.window);
717
+ constants.Dimensions.screen = describe(metrics.screen);
718
+ return constants;
719
+ }
720
+
721
+ // ---------------------------------------------------------------------------
722
+ // Settings
723
+ // ---------------------------------------------------------------------------
724
+
725
+ DesktopSettingsManagerModule::DesktopSettingsManagerModule(
726
+ std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
727
+ : NativeSettingsManagerCxxSpec(std::move(jsInvoker)), store_(settingsFilePath()) {}
728
+
729
+ Object DesktopSettingsManagerModule::getConstants(Runtime &rt) {
730
+ // `Settings._settings` is this object, once, at import. Everything
731
+ // `Settings.get` answers afterwards is read out of it in JavaScript, so a
732
+ // run that reads a setting never calls this module again -- which is why
733
+ // reading is as cheap here as it is on iOS.
734
+ Object constants(rt);
735
+ constants.setProperty(rt, "settings", facebook::jsi::valueFromDynamic(rt, store_.values()));
736
+ return constants;
737
+ }
738
+
739
+ void DesktopSettingsManagerModule::setValues(Runtime &rt, Object values) {
740
+ store_.merge(facebook::jsi::dynamicFromValue(rt, Value(rt, values)));
741
+ }
742
+
743
+ void DesktopSettingsManagerModule::deleteValues(Runtime &rt, Array values) {
744
+ const size_t count = values.size(rt);
745
+ std::vector<std::string> keys;
746
+ keys.reserve(count);
747
+ for (size_t index = 0; index < count; ++index) {
748
+ const Value element = values.getValueAtIndex(rt, index);
749
+ // `Settings` has no deleteValues of its own: this is reached by an app
750
+ // calling the module directly, so what arrives is whatever it passed.
751
+ if (element.isString()) {
752
+ keys.push_back(element.getString(rt).utf8(rt));
753
+ }
754
+ }
755
+ store_.erase(keys);
756
+ }
757
+
758
+ // ---------------------------------------------------------------------------
759
+ // Action sheets
760
+ // ---------------------------------------------------------------------------
761
+
762
+ namespace {
763
+
764
+ // `anchor`, which iOS uses to hang the popover off a view and a desktop uses to
765
+ // put the menu under the control that opened it. A react tag, resolved through
766
+ // the shadow tree, so all three hosts answer it with no code of their own.
767
+ //
768
+ // Nothing when there is no UIManager, when the tag names no view, or when that
769
+ // view has no layout yet -- and the caller then shows the menu at the pointer,
770
+ // which is where a menu with no anchor goes.
771
+ std::optional<std::pair<double, double>> anchorPoint(int tag) {
772
+ const std::shared_ptr<facebook::react::UIManager> uiManager = sharedUIManager();
773
+ if (uiManager == nullptr) {
774
+ return std::nullopt;
775
+ }
776
+ // Deprecated upstream and the only way to get from a tag to a node: every
777
+ // other entry point takes a shadow node, and what JavaScript has is a tag.
778
+ const std::shared_ptr<const facebook::react::ShadowNode> node =
779
+ uiManager->findShadowNodeByTag_DEPRECATED(tag);
780
+ if (node == nullptr) {
781
+ return std::nullopt;
782
+ }
783
+
784
+ // Relative to the root, which is the window's own coordinates -- the same
785
+ // space `showContextMenu` is passed from JavaScript.
786
+ const facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy;
787
+ const facebook::react::LayoutMetrics metrics =
788
+ uiManager->getRelativeLayoutMetrics(*node, nullptr, policy);
789
+ if (metrics == facebook::react::EmptyLayoutMetrics) {
790
+ return std::nullopt;
791
+ }
792
+
793
+ // Under the anchor rather than over it, which is where a desktop puts the
794
+ // menu a button opens. iOS points a popover at the view from whichever side
795
+ // has room, which is an iPad's problem and not one a window has.
796
+ return std::make_pair(static_cast<double>(metrics.frame.origin.x),
797
+ static_cast<double>(metrics.frame.origin.y + metrics.frame.size.height));
798
+ }
799
+
800
+ std::vector<int> indexList(Runtime &rt, const Object &options, const char *key) {
801
+ std::vector<int> indices;
802
+ const Value value = options.getProperty(rt, key);
803
+ if (!value.isObject() || !value.asObject(rt).isArray(rt)) {
804
+ return indices;
805
+ }
806
+ Array list = value.asObject(rt).asArray(rt);
807
+ const size_t count = list.size(rt);
808
+ for (size_t index = 0; index < count; ++index) {
809
+ const Value element = list.getValueAtIndex(rt, index);
810
+ if (element.isNumber()) {
811
+ indices.push_back(static_cast<int>(element.asNumber()));
812
+ }
813
+ }
814
+ return indices;
815
+ }
816
+
817
+ } // namespace
818
+
819
+ Object DesktopActionSheetModule::getConstants(Runtime &rt) {
820
+ return Object(rt);
821
+ }
822
+
823
+ void DesktopActionSheetModule::showActionSheetWithOptions(Runtime &rt,
824
+ Object options,
825
+ Function callback) {
826
+ ActionSheetRequest request;
827
+ request.title = optionalString(rt, options, "title");
828
+ request.message = optionalString(rt, options, "message");
829
+
830
+ const Value titles = options.getProperty(rt, "options");
831
+ if (titles.isObject() && titles.asObject(rt).isArray(rt)) {
832
+ Array list = titles.asObject(rt).asArray(rt);
833
+ const size_t count = list.size(rt);
834
+ for (size_t index = 0; index < count; ++index) {
835
+ const Value element = list.getValueAtIndex(rt, index);
836
+ if (element.isString()) {
837
+ request.options.push_back(element.getString(rt).utf8(rt));
838
+ }
839
+ }
840
+ }
841
+ if (request.options.empty()) {
842
+ // `options` is required and React Native does not check it. Nothing to show
843
+ // and no index to report, so the callback is not called -- which is what
844
+ // iOS does with a sheet nobody pressed anything on.
845
+ LOG(WARNING) << "ActionSheetIOS.showActionSheetWithOptions was given no options";
846
+ return;
847
+ }
848
+
849
+ request.disabledIndices = indexList(rt, options, "disabledButtonIndices");
850
+ const Value cancel = options.getProperty(rt, "cancelButtonIndex");
851
+ if (cancel.isNumber()) {
852
+ request.cancelIndex = static_cast<int>(cancel.asNumber());
853
+ }
854
+
855
+ const Value anchor = options.getProperty(rt, "anchor");
856
+ if (anchor.isNumber()) {
857
+ const std::optional<std::pair<double, double>> point =
858
+ anchorPoint(static_cast<int>(anchor.asNumber()));
859
+ if (point.has_value()) {
860
+ request.x = point->first;
861
+ request.y = point->second;
862
+ }
863
+ }
864
+
865
+ const ActionSheetMenu menu = actionSheetMenu(request);
866
+ // What it is about to show, because the presentation is the one part an
867
+ // automated run skips: with BASALT_TEST_MENU set no menu appears, and this
868
+ // line is where the anchor having been resolved is visible at all.
869
+ LOG(INFO) << "action sheet: " << request.options.size() << " options at (" << menu.menu.x << ", "
870
+ << menu.menu.y << ")";
871
+
872
+ // The callback has to outlive the menu, which is up until somebody chooses.
873
+ auto shared = std::make_shared<Function>(std::move(callback));
874
+ auto invoker = jsInvoker_;
875
+
876
+ presentMenu(menu.menu, [shared, invoker, request, menu](int chosen) {
877
+ const std::optional<int> option = actionSheetChoice(request, menu, chosen);
878
+ if (!option.has_value() || invoker == nullptr) {
879
+ return;
880
+ }
881
+ // Back onto the JavaScript thread: a menu answers from the thread that drew
882
+ // it, and a jsi::Function may only be called on the runtime's.
883
+ invoker->invokeAsync(
884
+ [shared, index = *option](Runtime &runtime) { shared->call(runtime, Value(index)); });
885
+ });
886
+ }
887
+
888
+ void DesktopActionSheetModule::showShareActionSheetWithOptions(Runtime &rt,
889
+ Object options,
890
+ Function failureCallback,
891
+ Function successCallback) {
892
+ ShareRequest request;
893
+ request.message = optionalString(rt, options, "message");
894
+ request.url = optionalString(rt, options, "url");
895
+ request.title = optionalString(rt, options, "subject");
896
+
897
+ if (request.message.empty() && request.url.empty()) {
898
+ // What iOS does: an error in the log and neither callback called. Worth
899
+ // keeping rather than improving, because an app that handled "no url or
900
+ // message" by waiting forever is already written that way.
901
+ LOG(ERROR) << "ActionSheetIOS.showShareActionSheetWithOptions had no `url` or `message`";
902
+ return;
903
+ }
904
+
905
+ auto failure = std::make_shared<Function>(std::move(failureCallback));
906
+ auto success = std::make_shared<Function>(std::move(successCallback));
907
+ auto invoker = jsInvoker_;
908
+
909
+ shareContent(request, [failure, success, invoker](ShareOutcome outcome,
910
+ const std::string &message) {
911
+ if (invoker == nullptr) {
912
+ return;
913
+ }
914
+ if (outcome == ShareOutcome::Failed) {
915
+ invoker->invokeAsync([failure, message](Runtime &runtime) {
916
+ // The shape `ShareActionSheetError` declares, which is what
917
+ // RCTJSErrorFromNSError produces on iOS: a domain, a code and a
918
+ // message. There is no NSError here, so the domain says where it came
919
+ // from and the code says what happened.
920
+ Object error(runtime);
921
+ error.setProperty(runtime, "domain", String::createFromUtf8(runtime, "basalt.share"));
922
+ error.setProperty(runtime, "code", String::createFromUtf8(runtime, "share-failed"));
923
+ error.setProperty(
924
+ runtime,
925
+ "message",
926
+ String::createFromUtf8(runtime, message.empty() ? "sharing failed" : message));
927
+ failure->call(runtime, error);
928
+ });
929
+ return;
930
+ }
931
+ // `(completed, method)`. iOS reports the activity type as the method and
932
+ // null when the sheet was dismissed; a desktop picker has no activity type
933
+ // to report, which is the same null `Share.share()` resolves with here.
934
+ const bool completed = outcome == ShareOutcome::Shared;
935
+ invoker->invokeAsync([success, completed](Runtime &runtime) {
936
+ success->call(runtime, Value(completed), Value::null());
937
+ });
938
+ });
939
+ }
940
+
941
+ void DesktopActionSheetModule::dismissActionSheet(Runtime &rt) {
942
+ (void)rt;
943
+ // Nothing to dismiss, and not for want of trying. A popup menu on these three
944
+ // runs its own tracking loop -- `TrackPopupMenu` on Win32 and
945
+ // `popUpMenuPositioningItem` on AppKit both block the thread that opened it
946
+ // until the menu goes away -- so this call, which arrives on the JavaScript
947
+ // thread, cannot reach the menu while it is up: the UI thread is inside the
948
+ // menu and the work this would post runs after it has closed. Taking it down
949
+ // needs each toolkit's cancel called from inside its own loop
950
+ // (`[NSMenu cancelTracking]`, `EndMenu()`, `gtk_popover_popdown`), which is a
951
+ // per-host piece of work; see docs/backlog/modules.md.
952
+ //
953
+ // React Native's own JavaScript guards this one -- `typeof
954
+ // RCTActionSheetManager.dismissActionSheet === 'function'` -- so iOS treats it
955
+ // as optional too. It is here because the generated spec declares it, and it
956
+ // says what it did rather than pretending.
957
+ LOG(WARNING) << "ActionSheetIOS.dismissActionSheet: a popup menu here closes on Escape or on a "
958
+ "choice and cannot be taken down from JavaScript";
959
+ }
960
+
592
961
  } // namespace basalt
@@ -1,9 +1,13 @@
1
1
  // The core React Native modules ReactCxxPlatform does not provide.
2
2
  //
3
- // Upstream supplies fifteen -- Animated, AppState, DeviceInfo, DevSettings,
4
- // ImageLoader, LogBox, ExceptionsManager, the observers, Networking,
5
- // PlatformConstants, SourceCode, WebSocket. What is here is the set a real app
6
- // reaches for and finds missing, and it splits in two ways that matter.
3
+ // Upstream supplies nineteen, from two providers rather than one:
4
+ // ReactCxxTurboModuleProvider has AppState, DeviceInfo, DevLoadingView,
5
+ // ExceptionsManager, ImageLoader, LogBox, NativeAnimatedModule, Networking,
6
+ // PlatformConstants and WebSocketModule, and DefaultTurboModules -- which it
7
+ // falls through to -- has the web APIs. `scripts/audit_modules.py` prints the
8
+ // list per module and per host; this comment said fifteen from one provider
9
+ // until that audit read both chains. What is here is the set a real app reaches
10
+ // for and finds missing, and it splits in two ways that matter.
7
11
  //
8
12
  // **How they fail.** `Clipboard` and `Vibration` are looked up with
9
13
  // `getEnforcing`, so their absence *throws at import* -- an app that so much as
@@ -24,9 +28,15 @@
24
28
 
25
29
  #pragma once
26
30
 
31
+ #include "ActionSheet.h"
32
+ #include "AppStateHost.h"
33
+ #include "DeviceMetrics.h"
27
34
  #include "PlatformServices.h"
35
+ #include "SettingsStore.h"
28
36
 
29
37
  #include <FBReactNativeSpec/FBReactNativeSpecJSI.h>
38
+ #include <react/coremodules/AppStateModule.h>
39
+ #include <react/coremodules/DeviceInfoModule.h>
30
40
 
31
41
  #include <optional>
32
42
  #include <string>
@@ -144,6 +154,132 @@ class DesktopShareModule : public facebook::react::NativeShareModuleCxxSpec<Desk
144
154
  std::optional<facebook::jsi::String> dialogTitle);
145
155
  };
146
156
 
157
+ // `Settings`, which an app reaches through `Settings.get` and `Settings.set`.
158
+ //
159
+ // The second module whose JavaScript had to be replaced as well, and found the
160
+ // same way: `Settings.js` branches on `Platform.OS === 'ios'` and hands
161
+ // everything else `SettingsFallback`, which warns and answers null. So the
162
+ // module was never reached, and the audit's claim that this one *throws* was
163
+ // wrong in the app's favour -- see src/overrides/Settings.ts and
164
+ // docs/backlog/modules.md.
165
+ //
166
+ // A file rather than each desktop's own settings store, for the reasons
167
+ // core/SettingsStore.h argues. The store is a member rather than a global: it
168
+ // is read in the constructor and written on every change, and a reload builds a
169
+ // new module, which re-reads the file -- which is the state.
170
+ //
171
+ // No `settingsUpdated` event, deliberately. iOS sets `_ignoringUpdates` around
172
+ // its own `setValues` for the reason this has nothing to emit: `Settings.set`
173
+ // has already merged into JavaScript's copy by the time the module is called,
174
+ // so `_sendObservations` would see no change and call no watcher. What does
175
+ // fire a watcher is a change from outside the process, which means watching the
176
+ // file; that is recorded rather than written.
177
+ class DesktopSettingsManagerModule
178
+ : public facebook::react::NativeSettingsManagerCxxSpec<DesktopSettingsManagerModule> {
179
+ public:
180
+ explicit DesktopSettingsManagerModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
181
+
182
+ static constexpr const char *kModuleName = "SettingsManager";
183
+
184
+ facebook::jsi::Object getConstants(facebook::jsi::Runtime &rt);
185
+ void setValues(facebook::jsi::Runtime &rt, facebook::jsi::Object values);
186
+ void deleteValues(facebook::jsi::Runtime &rt, facebook::jsi::Array values);
187
+
188
+ private:
189
+ SettingsStore store_;
190
+ };
191
+
192
+ // `ActionSheetIOS`, which is a popup menu here. See core/ActionSheet.h for why
193
+ // a menu rather than a dialog, and for what a desktop cannot do with one.
194
+ //
195
+ // The third module with a share sheet in it, and they are not duplicates: iOS
196
+ // splits sharing between `Share.share()` -- a promise, Android-shaped, which is
197
+ // `ShareModule` above -- and `ActionSheetIOS.showShareActionSheetWithOptions`,
198
+ // a callback pair on this module. Both end up in `shareContent`, so the picker
199
+ // an app sees is the same one either way.
200
+ //
201
+ // No JavaScript override, unlike `Settings`, `Share` and `Alert`:
202
+ // `ActionSheetIOS.js` has no `Platform.OS` branch and calls the module
203
+ // directly, so the module alone is enough. An iOS-named API is not
204
+ // automatically one whose JavaScript refuses to run off iOS, and the only way
205
+ // to know which kind it is, is to read it.
206
+ // `Dimensions` and `PixelRatio`, which every app reads and which answered with
207
+ // a fiction until 2026-10-10.
208
+ //
209
+ // ReactCxxPlatform *does* provide a `DeviceInfo` module -- the module audit
210
+ // records it as answered -- and what it answers is a hardcoded 1280x720 with a
211
+ // `// TODO: Wire this to come from the actual app size` beside it and no seam
212
+ // to wire. `scale` came back zero and `fontScale` was never initialised, so
213
+ // `PixelRatio.get()` was 0 and `PixelRatio.roundToNearestPixel(x)` was NaN in
214
+ // every app on every desktop. "The module answers" and "the module answers
215
+ // correctly" are different claims, and only the first was ever checked.
216
+ //
217
+ // The numbers are core's: see core/DeviceMetrics.h. What is here is the module
218
+ // and the event -- `didUpdateDimensions`, which is how `Dimensions.js` learns
219
+ // that a window was resized, and without which every layout computed from
220
+ // `Dimensions.get('window')` is stale the moment somebody drags a corner.
221
+ // `AppState`, whose every method ReactCxxPlatform leaves empty: `getConstants`
222
+ // answers `initialAppState: ""`, `getCurrentAppState` calls neither of its
223
+ // callbacks, and nothing ever emits a change. An app pausing a poll while it is
224
+ // not in front, or refetching when it comes back, was told nothing at all. See
225
+ // core/AppStateHost.h for what the three states mean on a desktop.
226
+ class DesktopAppStateModule
227
+ : public facebook::react::NativeAppStateCxxSpec<DesktopAppStateModule> {
228
+ public:
229
+ explicit DesktopAppStateModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
230
+ ~DesktopAppStateModule() override;
231
+
232
+ static constexpr const char *kModuleName = "AppState";
233
+
234
+ facebook::react::AppStateConstants getConstants(facebook::jsi::Runtime &rt);
235
+ void getCurrentAppState(
236
+ facebook::jsi::Runtime &rt,
237
+ const facebook::react::AsyncCallback<facebook::react::AppState> &success,
238
+ facebook::jsi::Function error);
239
+ // React Native's `NativeEventEmitter` calls both, and this platform's events
240
+ // go through `RCTDeviceEventEmitter` rather than through the module -- see
241
+ // AppState.js, which passes null for anything that is not iOS. Nothing to
242
+ // count, so nothing to do.
243
+ void addListener(facebook::jsi::Runtime &rt, const std::string &eventName);
244
+ void removeListeners(facebook::jsi::Runtime &rt, double count);
245
+ };
246
+
247
+ class DesktopDeviceInfoModule
248
+ : public facebook::react::NativeDeviceInfoCxxSpec<DesktopDeviceInfoModule> {
249
+ public:
250
+ explicit DesktopDeviceInfoModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
251
+ ~DesktopDeviceInfoModule() override;
252
+
253
+ static constexpr const char *kModuleName = "DeviceInfo";
254
+
255
+ facebook::react::DeviceInfoConstants getConstants(facebook::jsi::Runtime &rt);
256
+
257
+ private:
258
+ // What was last sent, so that a window *moved* rather than resized says
259
+ // nothing: the bounds signal fires for both, and `Dimensions.set` re-emits
260
+ // its `change` event for every call whether or not anything differs.
261
+ DeviceMetrics lastSent_;
262
+ };
263
+
264
+ class DesktopActionSheetModule
265
+ : public facebook::react::NativeActionSheetManagerCxxSpec<DesktopActionSheetModule> {
266
+ public:
267
+ explicit DesktopActionSheetModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
268
+ : NativeActionSheetManagerCxxSpec(std::move(jsInvoker)) {}
269
+
270
+ static constexpr const char *kModuleName = "ActionSheetManager";
271
+
272
+ facebook::jsi::Object getConstants(facebook::jsi::Runtime &rt);
273
+ void showActionSheetWithOptions(facebook::jsi::Runtime &rt,
274
+ facebook::jsi::Object options,
275
+ facebook::jsi::Function callback);
276
+ void showShareActionSheetWithOptions(facebook::jsi::Runtime &rt,
277
+ facebook::jsi::Object options,
278
+ facebook::jsi::Function failureCallback,
279
+ facebook::jsi::Function successCallback);
280
+ void dismissActionSheet(facebook::jsi::Runtime &rt);
281
+ };
282
+
147
283
  // Right-to-left layout. Portable in full: Yoga already does the work, and this
148
284
  // is the switch that turns it on.
149
285
  class DesktopI18nManagerModule