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.
- 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/desktop.d.ts.map +1 -1
- package/dist/cli/desktop.js +53 -4
- package/dist/cli/desktop.js.map +1 -1
- 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 +85 -1
- package/native/cmake/Skia.cmake +29 -6
- 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/SkiaViewComponent.cpp +11 -0
- package/native/core/SkiaViewComponent.h +50 -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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// `Settings`, which on iOS is `NSUserDefaults` and on a desktop is a file.
|
|
2
|
+
//
|
|
3
|
+
// React Native's `Settings` is four calls -- `get`, `set`, `watchKeys`,
|
|
4
|
+
// `clearWatch` -- over one native module, `SettingsManager`, which is a
|
|
5
|
+
// `getEnforcing` lookup. Nobody answered it here, so an app that reached for
|
|
6
|
+
// the API got React Native's `SettingsFallback`: three `console.warn`s and a
|
|
7
|
+
// null. The module was never even reached, for the same reason `Share` was not
|
|
8
|
+
// -- `Settings.js` branches on `Platform.OS === 'ios'` and everything else
|
|
9
|
+
// takes the fallback -- so this needs the JavaScript override as well; see
|
|
10
|
+
// src/overrides/Settings.ts.
|
|
11
|
+
//
|
|
12
|
+
// ## Why a file rather than each desktop's own settings store
|
|
13
|
+
//
|
|
14
|
+
// Each of the three has one, and none of them is the same shape as this API.
|
|
15
|
+
// macOS has `NSUserDefaults`, which is the thing `Settings` wraps and which a
|
|
16
|
+
// host could use directly. GTK's nearest equivalent is `GSettings`, which
|
|
17
|
+
// refuses a key that is not in a compiled schema -- and this API's whole
|
|
18
|
+
// contract is that an app invents its own keys at runtime, so a schema cannot
|
|
19
|
+
// exist. Windows has the registry, which has no JSON and no arrays.
|
|
20
|
+
//
|
|
21
|
+
// So: a JSON object in a file, identically on all three. One implementation
|
|
22
|
+
// with one set of semantics beats one real answer and two approximations that
|
|
23
|
+
// differ in what they can store. The cost is that macOS settings are not
|
|
24
|
+
// visible to `defaults read`, which is recorded in docs/backlog/modules.md
|
|
25
|
+
// along with the call that would change it.
|
|
26
|
+
//
|
|
27
|
+
// ## What is in the file
|
|
28
|
+
//
|
|
29
|
+
// A single JSON object, written whole on every change. Not a log and not a
|
|
30
|
+
// database: `Settings` is a handful of keys an app remembers between runs --
|
|
31
|
+
// a window's last tab, whether the tour was seen -- and an app with more than
|
|
32
|
+
// that wants AsyncStorage or SQLite, neither of which this pretends to be.
|
|
33
|
+
//
|
|
34
|
+
// Written to a sibling and renamed over, so a process that dies mid-write
|
|
35
|
+
// leaves the previous file rather than half of a new one. That matters more
|
|
36
|
+
// here than it looks: this file is read at startup, and a truncated one would
|
|
37
|
+
// make every later run start empty.
|
|
38
|
+
//
|
|
39
|
+
// ## What is not here
|
|
40
|
+
//
|
|
41
|
+
// `watchKeys` only ever fires for a change this process did not make. That is
|
|
42
|
+
// not a shortcut: iOS sets `_ignoringUpdates` around its own `setValues` for
|
|
43
|
+
// exactly the same reason, because `Settings.set` has already updated
|
|
44
|
+
// JavaScript's copy by the time the native module is called. Firing for an
|
|
45
|
+
// external change means watching the file, which is three different APIs, and
|
|
46
|
+
// is recorded rather than written; see docs/backlog/modules.md.
|
|
47
|
+
|
|
48
|
+
#pragma once
|
|
49
|
+
|
|
50
|
+
#include <folly/dynamic.h>
|
|
51
|
+
|
|
52
|
+
#include <filesystem>
|
|
53
|
+
#include <string>
|
|
54
|
+
#include <vector>
|
|
55
|
+
|
|
56
|
+
namespace basalt {
|
|
57
|
+
|
|
58
|
+
// Where the settings live, or an empty path when there is nowhere to put them.
|
|
59
|
+
//
|
|
60
|
+
// `BASALT_SETTINGS_FILE` wins when it is set, which is how the end-to-end
|
|
61
|
+
// scenario points two runs of a host at the same temporary file instead of at
|
|
62
|
+
// the person's real one, and how a portable install keeps its settings beside
|
|
63
|
+
// itself.
|
|
64
|
+
//
|
|
65
|
+
// Otherwise it is the per-user configuration directory each desktop names --
|
|
66
|
+
// `$XDG_CONFIG_HOME` or `~/.config` on Linux, `~/Library/Application Support`
|
|
67
|
+
// on macOS, `%APPDATA%` on Windows -- plus the application's identifier, plus
|
|
68
|
+
// `settings.json`. The identifier is what `app.identity.json` says, so two
|
|
69
|
+
// applications on one machine do not share a file; an app built by hand has no
|
|
70
|
+
// identity and lands under `basalt`, which is the honest answer for a binary
|
|
71
|
+
// that has not said who it is.
|
|
72
|
+
//
|
|
73
|
+
// Empty when even the home directory is unknown, which is a container with no
|
|
74
|
+
// environment rather than a mistake. A store with no path keeps its values in
|
|
75
|
+
// memory for the life of the process and persists nothing.
|
|
76
|
+
std::filesystem::path settingsFilePath();
|
|
77
|
+
|
|
78
|
+
// One spelling, in one place, for the same reason kTestQuitFileVar is.
|
|
79
|
+
inline constexpr const char *kSettingsFileVar = "BASALT_SETTINGS_FILE";
|
|
80
|
+
|
|
81
|
+
// The key-value store behind `SettingsManager`.
|
|
82
|
+
//
|
|
83
|
+
// Loaded in the constructor and saved on every change. Not thread-safe and not
|
|
84
|
+
// required to be: every caller is a TurboModule method, which runs on the
|
|
85
|
+
// JavaScript thread.
|
|
86
|
+
class SettingsStore {
|
|
87
|
+
public:
|
|
88
|
+
// Reads `file` if it is there. A missing file is the normal first run; a file
|
|
89
|
+
// that is not a JSON object is logged and treated as empty, because an app
|
|
90
|
+
// that refuses to start over a settings file somebody hand-edited would be
|
|
91
|
+
// worse than one that starts with no settings.
|
|
92
|
+
explicit SettingsStore(std::filesystem::path file);
|
|
93
|
+
|
|
94
|
+
// Every setting, as the object `getConstants().settings` hands to JavaScript.
|
|
95
|
+
const folly::dynamic &values() const {
|
|
96
|
+
return values_;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Merges `values` in, one key deep, and answers the keys whose value actually
|
|
100
|
+
// changed. A key whose new value equals the old one is not in that list,
|
|
101
|
+
// which is what decides whether anything needs writing.
|
|
102
|
+
//
|
|
103
|
+
// A null value deletes the key rather than storing a null. That is iOS's
|
|
104
|
+
// behaviour -- `RCTSettingsManager` calls `removeObjectForKey:` when
|
|
105
|
+
// `RCTConvert NSPropertyList:` answers nil -- and an app that sets a key to
|
|
106
|
+
// null means to forget it.
|
|
107
|
+
std::vector<std::string> merge(const folly::dynamic &values);
|
|
108
|
+
|
|
109
|
+
// Removes `keys`, answering the ones that were actually there.
|
|
110
|
+
std::vector<std::string> erase(const std::vector<std::string> &keys);
|
|
111
|
+
|
|
112
|
+
// Where it is kept, for the host that wants to say so in a log.
|
|
113
|
+
const std::filesystem::path &file() const {
|
|
114
|
+
return file_;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private:
|
|
118
|
+
// Writes the whole object, through a sibling and a rename. Failure is logged
|
|
119
|
+
// and not reported: there is nothing an app could do about a read-only home
|
|
120
|
+
// directory, and losing a setting is not worth refusing to run over.
|
|
121
|
+
void save() const;
|
|
122
|
+
|
|
123
|
+
std::filesystem::path file_;
|
|
124
|
+
folly::dynamic values_ = folly::dynamic::object();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
} // namespace basalt
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The shape a box shadow is cast by: a corner radius grown by the spread.
|
|
2
|
+
//
|
|
3
|
+
// Not an addition, which is the whole reason this is written down. The CSS spec
|
|
4
|
+
// gives a curve: a corner tighter than the spread is rounded off more gently
|
|
5
|
+
// than the spread alone would make it, so a small radius on a widely spread
|
|
6
|
+
// shadow does not turn into a circle.
|
|
7
|
+
//
|
|
8
|
+
// https://drafts.csswg.org/css-backgrounds/#shadow-shape
|
|
9
|
+
//
|
|
10
|
+
// Shared because two hosts compute it and the third does not: GSK's shadow
|
|
11
|
+
// nodes take the view's own outline and grow it themselves, where AppKit builds
|
|
12
|
+
// a `CGPath` and Direct2D a geometry, and both of those have to do this. It is
|
|
13
|
+
// a port of React Native's iOS half, which took it from the same place; a
|
|
14
|
+
// second hand-rolled copy of a cubic is exactly the kind of thing that ends up
|
|
15
|
+
// subtly different on one platform.
|
|
16
|
+
//
|
|
17
|
+
// A negative spread is allowed and shrinks the box, which is what an inset
|
|
18
|
+
// shadow's hole is built from, so `std::abs` is in the ratio rather than a
|
|
19
|
+
// guard against it.
|
|
20
|
+
|
|
21
|
+
#pragma once
|
|
22
|
+
|
|
23
|
+
#include <cmath>
|
|
24
|
+
|
|
25
|
+
namespace basalt {
|
|
26
|
+
|
|
27
|
+
// `radius` grown by `spread`, never below zero.
|
|
28
|
+
inline double spreadRadius(double radius, double spread) {
|
|
29
|
+
double adjustment = spread;
|
|
30
|
+
if (radius < std::abs(spread)) {
|
|
31
|
+
const double ratio = radius / std::abs(spread);
|
|
32
|
+
adjustment *= 1.0 + std::pow(ratio - 1.0, 3.0);
|
|
33
|
+
}
|
|
34
|
+
const double grown = radius + adjustment;
|
|
35
|
+
return grown > 0.0 ? grown : 0.0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
} // namespace basalt
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#include "SkiaViewComponent.h"
|
|
2
|
+
|
|
3
|
+
namespace facebook::react {
|
|
4
|
+
|
|
5
|
+
// The name the package's JavaScript registers, from
|
|
6
|
+
// `codegenNativeComponent("SkiaView")` in
|
|
7
|
+
// lib/module/specs/SkiaViewNativeComponent.js. 3.x's name; 2.x's is
|
|
8
|
+
// "SkiaPictureView", beside this.
|
|
9
|
+
extern const char SkiaViewComponentName[] = "SkiaView";
|
|
10
|
+
|
|
11
|
+
} // namespace facebook::react
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// `SkiaView`, which is what a `<Canvas>` from react-native-skia 3.x mounts.
|
|
2
|
+
//
|
|
3
|
+
// The 2.x one is `SkiaPictureView`, in core/SkiaPictureViewComponent.h beside
|
|
4
|
+
// this, and the rename is the whole difference as far as a component registry
|
|
5
|
+
// is concerned: `lib/module/specs/SkiaViewNativeComponent.js` is
|
|
6
|
+
// `codegenNativeComponent("SkiaView")` where it used to say the longer name.
|
|
7
|
+
// Both are registered, because which one an app mounts is decided by the Skia
|
|
8
|
+
// it installed and a host binary does not know which that is until the app's
|
|
9
|
+
// JavaScript runs.
|
|
10
|
+
//
|
|
11
|
+
// Plain `ViewProps`, for the reason the 2.x header spells out at length: a
|
|
12
|
+
// `<Canvas>` does not carry its content in props. It records in JavaScript and
|
|
13
|
+
// pushes the result through the Skia JSI API keyed by an id, and the only thing
|
|
14
|
+
// the native side needs from props is which id it is -- `nativeID`, an ordinary
|
|
15
|
+
// `ViewProps` field that arrives whatever a view config says.
|
|
16
|
+
//
|
|
17
|
+
// What is dropped, which is a shorter list than 2.x's: `opaque`,
|
|
18
|
+
// `highBitDepth`, `androidSurfaceType`, `androidZOrderOnTop` and
|
|
19
|
+
// `pointerEvents`. The two Android ones select a backing view there and mean
|
|
20
|
+
// nothing to a CAMetalLayer. `opaque` and `highBitDepth` are the backing
|
|
21
|
+
// store's format -- an opaque layer and ten bits a channel -- and a canvas
|
|
22
|
+
// without them draws the same picture into an eight-bit one. `pointerEvents`
|
|
23
|
+
// is already a `ViewProps` field and is handled where every other view's is.
|
|
24
|
+
// When one is wanted it belongs in a props class here and in the peer, which
|
|
25
|
+
// is the same call core/ExpoImageComponent.h makes about blurhash.
|
|
26
|
+
//
|
|
27
|
+
// Registered unconditionally, like the 2.x one and like ExpoImage: it costs a
|
|
28
|
+
// name in a registry, and the alternative is a host that has to be rebuilt to
|
|
29
|
+
// run an app that draws. Without Skia in the build the view mounts and stays
|
|
30
|
+
// empty -- and an app that draws has already failed at
|
|
31
|
+
// `getEnforcing('RNSkiaModule')` long before, which is the error that says
|
|
32
|
+
// what is actually missing.
|
|
33
|
+
|
|
34
|
+
#pragma once
|
|
35
|
+
|
|
36
|
+
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
|
|
37
|
+
#include <react/renderer/components/view/ViewEventEmitter.h>
|
|
38
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
39
|
+
#include <react/renderer/core/ConcreteComponentDescriptor.h>
|
|
40
|
+
|
|
41
|
+
namespace facebook::react {
|
|
42
|
+
|
|
43
|
+
extern const char SkiaViewComponentName[];
|
|
44
|
+
|
|
45
|
+
using SkiaViewShadowNode =
|
|
46
|
+
ConcreteViewShadowNode<SkiaViewComponentName, ViewProps, ViewEventEmitter>;
|
|
47
|
+
|
|
48
|
+
using SkiaViewComponentDescriptor = ConcreteComponentDescriptor<SkiaViewShadowNode>;
|
|
49
|
+
|
|
50
|
+
} // namespace facebook::react
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#include "SystemLocale.h"
|
|
2
|
+
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <cctype>
|
|
5
|
+
#include <cstdlib>
|
|
6
|
+
#include <mutex>
|
|
7
|
+
|
|
8
|
+
namespace basalt {
|
|
9
|
+
|
|
10
|
+
namespace {
|
|
11
|
+
|
|
12
|
+
struct State {
|
|
13
|
+
std::mutex mutex;
|
|
14
|
+
SystemLocale locale;
|
|
15
|
+
bool allowRTL{true};
|
|
16
|
+
std::optional<bool> forced;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
State &state() {
|
|
20
|
+
static State value;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The scripts that read right to left, by language code. Short because it is
|
|
25
|
+
// short: Arabic, Hebrew, Persian, Urdu and the two Kurdish and Pashto codes
|
|
26
|
+
// cover what a test needs, and nothing here decides a real locale -- the
|
|
27
|
+
// platforms answer that themselves. See the header.
|
|
28
|
+
bool languageReadsRightToLeft(const std::string &identifier) {
|
|
29
|
+
std::string language;
|
|
30
|
+
for (const char letter : identifier) {
|
|
31
|
+
if (letter == '-' || letter == '_' || letter == '.') {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
language += static_cast<char>(std::tolower(static_cast<unsigned char>(letter)));
|
|
35
|
+
}
|
|
36
|
+
return language == "ar" || language == "he" || language == "iw" || language == "fa" ||
|
|
37
|
+
language == "ur" || language == "ps" || language == "ckb" || language == "yi";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
} // namespace
|
|
41
|
+
|
|
42
|
+
std::optional<SystemLocale> testLocale() {
|
|
43
|
+
static const std::optional<SystemLocale> requested = []() -> std::optional<SystemLocale> {
|
|
44
|
+
const char *const value = std::getenv("BASALT_TEST_LOCALE");
|
|
45
|
+
if (value == nullptr || *value == '\0') {
|
|
46
|
+
return std::nullopt;
|
|
47
|
+
}
|
|
48
|
+
SystemLocale locale;
|
|
49
|
+
locale.identifier = value;
|
|
50
|
+
locale.rightToLeft = languageReadsRightToLeft(locale.identifier);
|
|
51
|
+
return locale;
|
|
52
|
+
}();
|
|
53
|
+
return requested;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
void setSystemLocale(SystemLocale locale) {
|
|
57
|
+
State &shared = state();
|
|
58
|
+
const std::lock_guard<std::mutex> guard(shared.mutex);
|
|
59
|
+
shared.locale = std::move(locale);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
SystemLocale lastKnownSystemLocale() {
|
|
63
|
+
if (const std::optional<SystemLocale> requested = testLocale()) {
|
|
64
|
+
return *requested;
|
|
65
|
+
}
|
|
66
|
+
State &shared = state();
|
|
67
|
+
const std::lock_guard<std::mutex> guard(shared.mutex);
|
|
68
|
+
return shared.locale;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
bool resolveRightToLeft(bool allowRTL, std::optional<bool> forced, bool systemRightToLeft) {
|
|
72
|
+
if (!allowRTL) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (forced.has_value()) {
|
|
76
|
+
return *forced;
|
|
77
|
+
}
|
|
78
|
+
return systemRightToLeft;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
bool layoutIsRightToLeft() {
|
|
82
|
+
const SystemLocale locale = lastKnownSystemLocale();
|
|
83
|
+
State &shared = state();
|
|
84
|
+
const std::lock_guard<std::mutex> guard(shared.mutex);
|
|
85
|
+
return resolveRightToLeft(shared.allowRTL, shared.forced, locale.rightToLeft);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
void setAllowRTL(bool allow) {
|
|
89
|
+
State &shared = state();
|
|
90
|
+
const std::lock_guard<std::mutex> guard(shared.mutex);
|
|
91
|
+
shared.allowRTL = allow;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
void setForceRTL(bool force) {
|
|
95
|
+
State &shared = state();
|
|
96
|
+
const std::lock_guard<std::mutex> guard(shared.mutex);
|
|
97
|
+
// `forceRTL(false)` clears the force rather than forcing left-to-right,
|
|
98
|
+
// which is what React Native's own platforms do: an app turning it off goes
|
|
99
|
+
// back to following the system.
|
|
100
|
+
shared.forced = force ? std::optional<bool>(true) : std::nullopt;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
} // namespace basalt
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Which language the desktop is in, and which way it reads.
|
|
2
|
+
//
|
|
3
|
+
// `I18nManager` answered neither until 2026-10-10: `localeIdentifier` was the
|
|
4
|
+
// empty string and `isRTL` was false unless an app had called `forceRTL`. The
|
|
5
|
+
// reason recorded in CoreModules.cpp was that "deciding it from the system
|
|
6
|
+
// locale would need a locale database this does not have", which is true of
|
|
7
|
+
// *parsing a locale name* and not of the question: every desktop publishes the
|
|
8
|
+
// reading direction directly.
|
|
9
|
+
//
|
|
10
|
+
// GTK `gtk_widget_get_default_direction()`, which GTK itself resolves
|
|
11
|
+
// from the locale -- translators ship "default:RTL" for a
|
|
12
|
+
// right-to-left language and GTK reads it back.
|
|
13
|
+
// AppKit `+[NSLocale characterDirectionForLanguage:]`, which answers from
|
|
14
|
+
// ICU's own data.
|
|
15
|
+
// Windows `GetLocaleInfoEx` with `LOCALE_IREADINGLAYOUT`, where 1 is
|
|
16
|
+
// right-to-left.
|
|
17
|
+
//
|
|
18
|
+
// What this costs if it is wrong is the whole layout mirrored the wrong way,
|
|
19
|
+
// which is why none of the three guesses from the language name.
|
|
20
|
+
//
|
|
21
|
+
// ## Why the layout and not only the flag
|
|
22
|
+
//
|
|
23
|
+
// `I18nManager.isRTL` is what an app branches on, and React Native expects the
|
|
24
|
+
// *layout* to agree: Yoga mirrors `flexDirection: 'row'`, `start`/`end`,
|
|
25
|
+
// padding and margin when it is told the direction, and the host is what tells
|
|
26
|
+
// it, through `LayoutConstraints::layoutDirection`. All three hosts passed
|
|
27
|
+
// `LeftToRight` unconditionally, so even an app calling `forceRTL(true)` and
|
|
28
|
+
// reloading -- which is what React Native's own documentation says to do --
|
|
29
|
+
// got a left-to-right layout with a flag that claimed otherwise.
|
|
30
|
+
|
|
31
|
+
#pragma once
|
|
32
|
+
|
|
33
|
+
#include <optional>
|
|
34
|
+
#include <string>
|
|
35
|
+
|
|
36
|
+
namespace basalt {
|
|
37
|
+
|
|
38
|
+
struct SystemLocale {
|
|
39
|
+
// BCP 47 or POSIX, as the platform spells it: `en-US` on Windows, `en_US` on
|
|
40
|
+
// the other two. Passed through rather than normalised, because React
|
|
41
|
+
// Native's own platforms do the same -- iOS reports `en_US` and Android
|
|
42
|
+
// `en_US` too -- and an app parsing it is parsing its platform's spelling.
|
|
43
|
+
std::string identifier;
|
|
44
|
+
bool rightToLeft{false};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// What the desktop says, asked once at startup by the host. **Called on the UI
|
|
48
|
+
// thread**: a toolkit's settings are not thread-safe.
|
|
49
|
+
SystemLocale systemLocale();
|
|
50
|
+
|
|
51
|
+
// What a test asked for, or nothing: `BASALT_TEST_LOCALE=ar-EG` overrides both
|
|
52
|
+
// fields, the direction coming from the language rather than from a database --
|
|
53
|
+
// the five right-to-left scripts are a short list and a test needs no more
|
|
54
|
+
// than that.
|
|
55
|
+
//
|
|
56
|
+
// Here for the same reason `BASALT_TEST_FONT_SCALE` is in core/FontScaling.h:
|
|
57
|
+
// a scenario cannot change the machine's language, and a feature with no test
|
|
58
|
+
// that fails when it is removed is not finished.
|
|
59
|
+
std::optional<SystemLocale> testLocale();
|
|
60
|
+
|
|
61
|
+
// Remembered by the host at startup, readable from any thread.
|
|
62
|
+
void setSystemLocale(SystemLocale locale);
|
|
63
|
+
SystemLocale lastKnownSystemLocale();
|
|
64
|
+
|
|
65
|
+
// Whether the layout should be mirrored, which is `I18nManager.isRTL`.
|
|
66
|
+
//
|
|
67
|
+
// React Native's three controls, in its own order of precedence:
|
|
68
|
+
//
|
|
69
|
+
// allowRTL(false) never mirrored, whatever the locale says
|
|
70
|
+
// forceRTL(true) always mirrored, whatever the locale says
|
|
71
|
+
// otherwise whatever the locale says
|
|
72
|
+
//
|
|
73
|
+
// `forceRTL(false)` is not "force left to right": React Native treats it as
|
|
74
|
+
// clearing the force, which is why the forced state is an optional here.
|
|
75
|
+
bool resolveRightToLeft(bool allowRTL, std::optional<bool> forced, bool systemRightToLeft);
|
|
76
|
+
|
|
77
|
+
// The answer for this process, from what the host reported and what an app has
|
|
78
|
+
// asked for since. Read by the hosts when they build layout constraints and
|
|
79
|
+
// by the I18nManager module.
|
|
80
|
+
bool layoutIsRightToLeft();
|
|
81
|
+
|
|
82
|
+
// The app's half, from `I18nManager`.
|
|
83
|
+
void setAllowRTL(bool allow);
|
|
84
|
+
void setForceRTL(bool force);
|
|
85
|
+
|
|
86
|
+
} // namespace basalt
|
|
@@ -72,6 +72,34 @@ std::optional<int> scriptedMenuChoice() {
|
|
|
72
72
|
return choice;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
int scriptedMenuIndex(const std::vector<MenuEntry> &entries, int scripted) {
|
|
76
|
+
// Out of range answers as a dismissal rather than being clamped, which is the
|
|
77
|
+
// opposite of what presentAlert does with a button. A menu's entries are not
|
|
78
|
+
// interchangeable -- picking the nearest one would run something the script
|
|
79
|
+
// did not ask for -- whereas an alert's last button is always the way out.
|
|
80
|
+
//
|
|
81
|
+
// `menuEntryCount` and not `entries.size()`: an index counts every level now,
|
|
82
|
+
// so the valid range is the whole tree and a script naming a submenu's item
|
|
83
|
+
// would otherwise be refused for being past the end of the top level.
|
|
84
|
+
const int count = menuEntryCount(entries);
|
|
85
|
+
if (scripted < 0 || scripted >= count) {
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// And a dismissal for anything a person could not have chosen either: a
|
|
90
|
+
// parent opens rather than chooses, a separator is a line, an entry naming a
|
|
91
|
+
// role this desktop cannot perform is not in the menu at all, and a disabled
|
|
92
|
+
// entry is drawn and refuses the click. The last of those was missing until
|
|
93
|
+
// an action sheet put its title in the menu as a disabled entry, which a
|
|
94
|
+
// script could then "choose".
|
|
95
|
+
const MenuEntry *entry = menuEntryAt(entries, scripted);
|
|
96
|
+
if (entry == nullptr || entry->isParent() || entry->isSeparator() || !menuEntryShown(*entry) ||
|
|
97
|
+
!entry->enabled) {
|
|
98
|
+
return -1;
|
|
99
|
+
}
|
|
100
|
+
return scripted;
|
|
101
|
+
}
|
|
102
|
+
|
|
75
103
|
void presentMenu(const MenuRequest &request, MenuCallback onChosen) {
|
|
76
104
|
const std::optional<int> scripted = scriptedMenuChoice();
|
|
77
105
|
if (!scripted.has_value()) {
|
|
@@ -79,12 +107,23 @@ void presentMenu(const MenuRequest &request, MenuCallback onChosen) {
|
|
|
79
107
|
return;
|
|
80
108
|
}
|
|
81
109
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
110
|
+
const int index = scriptedMenuIndex(request.entries, *scripted);
|
|
111
|
+
|
|
112
|
+
// The platform's half of a role still happens: this seam exists to skip the
|
|
113
|
+
// window a person would have clicked, not to skip what choosing the item does.
|
|
114
|
+
// Without this, `copy` under test would resolve a promise and copy nothing.
|
|
115
|
+
//
|
|
116
|
+
// On the UI thread, which this is not. `showContextMenu` is a TurboModule
|
|
117
|
+
// call, so this runs on the JavaScript thread, and `performMenuRole` touches
|
|
118
|
+
// the window and the focused control -- AppKit from the wrong thread does not
|
|
119
|
+
// return. The symptom was the promise never settling at all: `about` opens a
|
|
120
|
+
// panel, and the call to open it never came back.
|
|
121
|
+
if (index >= 0) {
|
|
122
|
+
const MenuEntry *entry = menuEntryAt(request.entries, index);
|
|
123
|
+
if (entry != nullptr && !entry->role.empty()) {
|
|
124
|
+
postToUiThread([role = entry->role] { performMenuRole(role); });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
88
127
|
onChosen(index);
|
|
89
128
|
}
|
|
90
129
|
|
package/native/core/TestDialog.h
CHANGED
|
@@ -72,6 +72,21 @@ void presentAlert(const AlertRequest &request, AlertCallback onButton);
|
|
|
72
72
|
// stops the process where it stands.
|
|
73
73
|
std::optional<int> scriptedMenuChoice();
|
|
74
74
|
|
|
75
|
+
// Which entry a script naming `scripted` actually lands on, or -1 for a
|
|
76
|
+
// dismissal.
|
|
77
|
+
//
|
|
78
|
+
// What a script can answer is what a real menu can, which is the whole point of
|
|
79
|
+
// the instrument and is four separate refusals: an index past the end of the
|
|
80
|
+
// tree, a separator, a submenu's parent -- which opens rather than chooses --
|
|
81
|
+
// and an entry that is drawn greyed or not drawn at all. Each of those is
|
|
82
|
+
// something a person could not have clicked, so a script that names one is
|
|
83
|
+
// answered the way closing the menu is.
|
|
84
|
+
//
|
|
85
|
+
// Separate from presentMenu so that it can be asserted. The variable is read
|
|
86
|
+
// once and cached for the life of the process, so a test cannot set it, and the
|
|
87
|
+
// arithmetic is the part worth testing anyway.
|
|
88
|
+
int scriptedMenuIndex(const std::vector<MenuEntry> &entries, int scripted);
|
|
89
|
+
|
|
75
90
|
// `showMenu`, or the scripted answer when there is one. Everything that would
|
|
76
91
|
// put a popup menu on screen goes through here rather than calling the platform
|
|
77
92
|
// directly, for the same reason `presentAlert` exists.
|