basalt-core 0.0.0-rc5 → 0.1.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.
- package/dist/cli/basalt.d.ts.map +1 -1
- package/dist/cli/basalt.js +18 -1
- package/dist/cli/basalt.js.map +1 -1
- package/dist/cli/bundle-cli.js +0 -0
- package/dist/cli/packageApp.d.ts.map +1 -1
- package/dist/cli/packageApp.js +7 -1
- package/dist/cli/packageApp.js.map +1 -1
- package/dist/metro-config.d.ts.map +1 -1
- package/dist/metro-config.js +53 -5
- package/dist/metro-config.js.map +1 -1
- package/dist/src/KeyHandler.d.ts +1 -1
- package/dist/src/KeyHandler.d.ts.map +1 -1
- package/dist/src/KeyHandler.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/overrides/Alert.d.ts +1 -1
- package/dist/src/overrides/Alert.js +1 -1
- package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
- package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
- package/dist/src/overrides/BaseViewConfig.js +21 -3
- package/dist/src/overrides/BaseViewConfig.js.map +1 -1
- package/dist/src/overrides/Button.d.ts +81 -0
- package/dist/src/overrides/Button.d.ts.map +1 -0
- package/dist/src/overrides/Button.js +109 -0
- package/dist/src/overrides/Button.js.map +1 -0
- package/dist/src/overrides/ScrollView.d.ts +25 -2
- package/dist/src/overrides/ScrollView.d.ts.map +1 -1
- package/dist/src/overrides/ScrollView.js +64 -7
- package/dist/src/overrides/ScrollView.js.map +1 -1
- package/dist/src/overrides/Settings.d.ts +81 -0
- package/dist/src/overrides/Settings.d.ts.map +1 -0
- package/dist/src/overrides/Settings.js +120 -0
- package/dist/src/overrides/Settings.js.map +1 -0
- package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
- package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
- package/dist/src/overrides/processDecelerationRate.js +31 -0
- package/dist/src/overrides/processDecelerationRate.js.map +1 -0
- package/dist/src/useContextMenu.d.ts +57 -6
- package/dist/src/useContextMenu.d.ts.map +1 -1
- package/dist/src/useContextMenu.js +34 -1
- package/dist/src/useContextMenu.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/native/CMakeLists.txt +51 -0
- package/native/core/ActionSheet.cpp +75 -0
- package/native/core/ActionSheet.h +90 -0
- package/native/core/AppStateHost.cpp +76 -0
- package/native/core/AppStateHost.h +68 -0
- package/native/core/BackgroundLayers.h +282 -0
- package/native/core/BlendModes.h +67 -0
- package/native/core/CoreModules.cpp +380 -11
- package/native/core/CoreModules.h +140 -4
- package/native/core/CrashHandler.cpp +246 -0
- package/native/core/CrashHandler.h +65 -0
- package/native/core/CursorNames.h +110 -0
- package/native/core/DevMenu.cpp +6 -5
- package/native/core/DeviceMetrics.cpp +87 -0
- package/native/core/DeviceMetrics.h +80 -0
- package/native/core/Filters.h +336 -0
- package/native/core/FontFitting.h +183 -0
- package/native/core/FontScaling.h +153 -0
- package/native/core/FontVariants.h +148 -0
- package/native/core/Gradients.h +523 -0
- package/native/core/ImageAnimation.h +109 -0
- package/native/core/ImageBytes.cpp +28 -1
- package/native/core/ImageBytes.h +17 -1
- package/native/core/ImageProgress.h +85 -0
- package/native/core/LabelRegistry.h +189 -0
- package/native/core/LegacyShadow.h +90 -0
- package/native/core/LiveRegions.h +114 -0
- package/native/core/MenuModule.cpp +35 -4
- package/native/core/MountingWalk.h +125 -2
- package/native/core/PlatformServices.h +187 -3
- package/native/core/SafeAreaComponent.cpp +115 -0
- package/native/core/SafeAreaComponent.h +105 -0
- package/native/core/ScreensComponent.cpp +201 -0
- package/native/core/ScreensComponent.h +216 -0
- package/native/core/ScrollBounds.cpp +8 -0
- package/native/core/ScrollBounds.h +14 -0
- package/native/core/ScrollIndicator.h +67 -0
- package/native/core/ScrollMomentum.cpp +24 -0
- package/native/core/ScrollMomentum.h +14 -0
- package/native/core/ScrollSnap.cpp +121 -12
- package/native/core/ScrollSnap.h +45 -0
- package/native/core/ScrollVisiblePosition.cpp +68 -0
- package/native/core/ScrollVisiblePosition.h +101 -0
- package/native/core/SettingsStore.cpp +199 -0
- package/native/core/SettingsStore.h +127 -0
- package/native/core/ShadowShape.h +38 -0
- package/native/core/SystemLocale.cpp +103 -0
- package/native/core/SystemLocale.h +86 -0
- package/native/core/TestDialog.cpp +45 -6
- package/native/core/TestDialog.h +15 -0
- package/native/core/TextAlignments.h +240 -0
- package/native/core/TextChecking.h +133 -0
- package/native/core/TextColors.h +83 -0
- package/native/core/TextDecorations.h +101 -0
- package/native/core/TextDirection.cpp +242 -0
- package/native/core/TextDirection.h +88 -0
- package/native/core/TextHighlight.h +30 -0
- package/native/core/TextSelection.cpp +97 -0
- package/native/core/TextSelection.h +161 -0
- package/native/core/TextShadows.h +96 -0
- package/native/core/TextVerticalAlign.h +33 -0
- package/native/core/UIManagerAccess.cpp +36 -0
- package/native/core/UIManagerAccess.h +28 -0
- package/native/core/WindowBoundsCache.cpp +58 -1
- package/native/core/WindowControl.h +24 -0
- package/native/core/WritingDirections.h +52 -0
- package/native/core/portability_probe.cpp +7 -0
- package/native/tests/RenamedFontFile.h +161 -0
- package/native/tests/TestHarness.cpp +18 -0
- package/native/tests/test_action_sheet.cpp +157 -0
- package/native/tests/test_app_state.cpp +60 -0
- package/native/tests/test_background_layers.cpp +281 -0
- package/native/tests/test_blend_modes.cpp +79 -0
- package/native/tests/test_blobs.cpp +1 -1
- package/native/tests/test_device_metrics.cpp +126 -0
- package/native/tests/test_filters.cpp +271 -0
- package/native/tests/test_font_fitting.cpp +172 -0
- package/native/tests/test_font_scaling.cpp +127 -0
- package/native/tests/test_font_variants.cpp +84 -0
- package/native/tests/test_gradients.cpp +390 -0
- package/native/tests/test_image_animation.cpp +105 -0
- package/native/tests/test_image_progress.cpp +94 -0
- package/native/tests/test_label_registry.cpp +262 -0
- package/native/tests/test_legacy_shadow.cpp +165 -0
- package/native/tests/test_live_regions.cpp +108 -0
- package/native/tests/test_menu_entries.cpp +192 -0
- package/native/tests/test_momentum.cpp +45 -0
- package/native/tests/test_scroll_bounds.cpp +42 -0
- package/native/tests/test_scroll_indicator.cpp +61 -0
- package/native/tests/test_scroll_snap.cpp +129 -0
- package/native/tests/test_scroll_visible_position.cpp +121 -0
- package/native/tests/test_settings_store.cpp +257 -0
- package/native/tests/test_shadow_shape.cpp +47 -0
- package/native/tests/test_system_locale.cpp +36 -0
- package/native/tests/test_text_alignment.cpp +299 -0
- package/native/tests/test_text_checking.cpp +31 -0
- package/native/tests/test_text_colors.cpp +92 -0
- package/native/tests/test_text_decorations.cpp +100 -0
- package/native/tests/test_text_selection.cpp +168 -0
- package/native/tests/test_text_shadows.cpp +132 -0
- 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
|
|
245
|
+
const SystemLocale locale = lastKnownSystemLocale();
|
|
240
246
|
|
|
241
247
|
Object constants(rt);
|
|
242
|
-
// `isRTL` is what the layout actually is
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|