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
@@ -0,0 +1,105 @@
1
+ // react-native-safe-area-context's provider view, as a Fabric component.
2
+ //
3
+ // ## Why this is the file that makes navigation work
4
+ //
5
+ // `SafeAreaProvider` renders `insets != null ? children : null`, and `insets`
6
+ // starts null unless the application passed `initialMetrics`. The only other
7
+ // thing that fills it is an `onInsetsChange` event from this component. With no
8
+ // native half, nothing ever fires it, so the provider renders null forever and
9
+ // every screen beneath it stays unmounted.
10
+ //
11
+ // That is why an app using react-navigation or expo-router showed an empty
12
+ // window and logged nothing: there is no error anywhere in that path, just a
13
+ // provider waiting for a platform that never answers. Measured on 2026-10-01
14
+ // before this existed; see docs/backlog/ecosystem.md.
15
+ //
16
+ // ## Why the event comes from layout rather than from a host
17
+ //
18
+ // Insets on a phone are the notch and the home indicator, which the toolkit
19
+ // knows and C++ does not. A desktop window has neither, so the insets are zero
20
+ // on all three platforms and the only unknown is the frame, which is this
21
+ // view's own layout. Both are therefore available here, in the shadow node,
22
+ // and a per-host mounting peer would be three copies of the same zero.
23
+ //
24
+ // The provider is styled `flex: 1` by the library, so its frame is the surface,
25
+ // which is what `SafeAreaFrameContext` is supposed to carry.
26
+ //
27
+ // If a desktop later grows a real inset -- a client-side-decorated title bar
28
+ // drawn over the content, say -- this is the place it would come from, and it
29
+ // would need a host behind it at that point.
30
+
31
+ #pragma once
32
+
33
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
34
+ #include <react/renderer/components/view/ViewEventEmitter.h>
35
+ #include <react/renderer/components/view/ViewProps.h>
36
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
37
+ #include <react/renderer/core/LayoutContext.h>
38
+ #include <react/renderer/graphics/Rect.h>
39
+ #include <ReactCommon/TurboModule.h>
40
+
41
+ namespace facebook::react {
42
+
43
+ extern const char SafeAreaProviderComponentName[];
44
+
45
+ // `onInsetsChange`, whose payload is `{frame: {x, y, width, height}, insets:
46
+ // {top, right, bottom, left}}`. The names are the library's and an app reads
47
+ // them straight off `event.nativeEvent`, so they are a contract rather than a
48
+ // choice.
49
+ class SafeAreaProviderEventEmitter : public ViewEventEmitter {
50
+ public:
51
+ using ViewEventEmitter::ViewEventEmitter;
52
+
53
+ void onInsetsChange(const Rect &frame, const EdgeInsets &insets) const;
54
+ };
55
+
56
+ // No props of its own. `onInsetsChange` is an event rather than a prop, and
57
+ // everything else the library sets on it is ordinary view styling, so
58
+ // `ViewProps` parses the lot.
59
+ class SafeAreaProviderShadowNode final
60
+ : public ConcreteViewShadowNode<SafeAreaProviderComponentName, ViewProps,
61
+ SafeAreaProviderEventEmitter> {
62
+ public:
63
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
64
+
65
+ static ShadowNodeTraits BaseTraits() {
66
+ return ConcreteViewShadowNode::BaseTraits();
67
+ }
68
+
69
+ void layout(LayoutContext layoutContext) override;
70
+ };
71
+
72
+ using SafeAreaProviderComponentDescriptor =
73
+ ConcreteComponentDescriptor<SafeAreaProviderShadowNode>;
74
+
75
+ } // namespace facebook::react
76
+
77
+ namespace basalt {
78
+
79
+ // The window the application started with, which a host reports once it has
80
+ // one. Zero means not yet known, and the module then omits
81
+ // `initialWindowMetrics` rather than claiming a zero-sized window: the field is
82
+ // optional in the library's own spec, and absent is a state it already handles.
83
+ void setInitialWindowFrame(double width, double height);
84
+
85
+ // `RNCSafeAreaContext`, which the library asks for with `TurboModuleRegistry.get`.
86
+ //
87
+ // A null answer is survivable -- that is `get` rather than `getEnforcing` -- and
88
+ // until now that is what it got, which is why the warning appeared in every log.
89
+ // What the module is actually for is `initialWindowMetrics`, the export an app
90
+ // passes to `SafeAreaProvider` so the first frame is right rather than corrected
91
+ // one pass later. Nothing else in the library needs it.
92
+ class DesktopSafeAreaModule : public facebook::react::TurboModule {
93
+ public:
94
+ static constexpr auto kModuleName = "RNCSafeAreaContext";
95
+
96
+ explicit DesktopSafeAreaModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
97
+
98
+ private:
99
+ static facebook::jsi::Value getConstants(facebook::jsi::Runtime &runtime,
100
+ facebook::react::TurboModule &module,
101
+ const facebook::jsi::Value *args,
102
+ size_t count);
103
+ };
104
+
105
+ } // namespace basalt
@@ -0,0 +1,201 @@
1
+ #include "ScreensComponent.h"
2
+
3
+ #include <react/renderer/core/LayoutableShadowNode.h>
4
+ #include <react/renderer/core/propsConversions.h>
5
+
6
+ #include <algorithm>
7
+ #include <string>
8
+
9
+ namespace facebook::react {
10
+
11
+ extern const char RNSScreenComponentName[] = "RNSScreen";
12
+
13
+ RNSScreenProps::RNSScreenProps(const PropsParserContext &context,
14
+ const RNSScreenProps &sourceProps,
15
+ const RawProps &rawProps)
16
+ : ViewProps(context, sourceProps, rawProps),
17
+ activityState(convertRawProp(context, rawProps, "activityState",
18
+ sourceProps.activityState, -1.0f)) {
19
+ // `transparentModal` and `containedTransparentModal` are the two that show
20
+ // what is under them. Matching on the substring rather than listing both
21
+ // means a presentation added upstream with the same word in it behaves the
22
+ // way its name says.
23
+ const auto presentation = convertRawProp(
24
+ context, rawProps, "stackPresentation", std::string{}, std::string{});
25
+ seeThrough = presentation.find("ransparent") != std::string::npos;
26
+
27
+ // Only an explicit 0 hides it. The default is -1, which the library uses for
28
+ // a screen it is not managing, and 1 is a screen mid-transition that still
29
+ // has to be on screen to be transitioned.
30
+ if (activityState == 0.0f) {
31
+ yogaStyle.setDisplay(yoga::Display::None);
32
+ }
33
+ }
34
+
35
+ extern const char RNSScreenContainerComponentName[] = "RNSScreenContainer";
36
+ extern const char RNSScreenNavigationContainerComponentName[] =
37
+ "RNSScreenNavigationContainer";
38
+ extern const char RNSScreenContentWrapperComponentName[] = "RNSScreenContentWrapper";
39
+ extern const char RNSScreenStackHeaderSubviewComponentName[] =
40
+ "RNSScreenStackHeaderSubview";
41
+
42
+ extern const char RNSScreenStackComponentName[] = "RNSScreenStack";
43
+ extern const char RNSScreenStackHeaderConfigComponentName[] =
44
+ "RNSScreenStackHeaderConfig";
45
+
46
+ RNSScreenStackHeaderConfigProps::RNSScreenStackHeaderConfigProps(
47
+ const PropsParserContext &context,
48
+ const RNSScreenStackHeaderConfigProps &sourceProps,
49
+ const RawProps &rawProps)
50
+ : ViewProps(context, sourceProps, rawProps),
51
+ hidden(convertRawProp(context, rawProps, "hidden", sourceProps.hidden, false)) {
52
+ // Nothing is styled here on purpose. Setting `yogaStyle` from a props
53
+ // constructor does not reach this node: the yoga node holds its own copy,
54
+ // and the props the node ends up with are not always the ones built here.
55
+ // The bar is shaped in `adopt()` and taken out of the layout in `layout()`,
56
+ // which are the two places that work. See the header.
57
+ }
58
+
59
+ void RNSScreenStackHeaderConfigShadowNode::layout(LayoutContext layoutContext) {
60
+ ConcreteViewShadowNode::layout(layoutContext);
61
+
62
+ // Hidden, or with nothing to show. Both end the same way and both have to
63
+ // happen here rather than through the style, for the reason the props
64
+ // constructor gives.
65
+ if (!getConcreteProps().hidden && !getChildren().empty()) {
66
+ return;
67
+ }
68
+ auto metrics = getLayoutMetrics();
69
+ if (metrics.displayType == DisplayType::None) {
70
+ return;
71
+ }
72
+ metrics.displayType = DisplayType::None;
73
+ setLayoutMetrics(metrics);
74
+ }
75
+
76
+ // See the header: the content wrapper fills the screen, so the bar sits on top
77
+ // of it until something moves it.
78
+ void RNSScreenShadowNode::layout(LayoutContext layoutContext) {
79
+ ConcreteViewShadowNode::layout(layoutContext);
80
+
81
+ // The bar, and the body out from under it, together.
82
+ //
83
+ // Both here rather than the bar in the header itself, because the fallback
84
+ // path has no screen node at all: react-native-screens renders plain views
85
+ // where the native components are unavailable, nothing would move the body,
86
+ // and a bar given a height there would only be a taller strip painted over
87
+ // the content than the one already in the way.
88
+ RNSScreenStackHeaderConfigShadowNode *header = nullptr;
89
+ for (const auto &child : getChildren()) {
90
+ auto *candidate = const_cast<RNSScreenStackHeaderConfigShadowNode *>(
91
+ dynamic_cast<const RNSScreenStackHeaderConfigShadowNode *>(child.get()));
92
+ if (candidate == nullptr || candidate->getConcreteProps().hidden ||
93
+ candidate->getLayoutMetrics().displayType == DisplayType::None) {
94
+ continue;
95
+ }
96
+ header = candidate;
97
+ break;
98
+ }
99
+ if (header == nullptr) {
100
+ return;
101
+ }
102
+
103
+ auto headerMetrics = header->getLayoutMetrics();
104
+ const auto natural = headerMetrics.frame.size.height;
105
+ if (natural <= 0) {
106
+ return;
107
+ }
108
+ const auto barHeight = natural + 2 * kHeaderPaddingY;
109
+
110
+ headerMetrics.frame.size.height = barHeight;
111
+ header->ensureUnsealed();
112
+ header->setLayoutMetrics(headerMetrics);
113
+
114
+ // Grown after the fact, so the title keeps the position yoga gave it and is
115
+ // moved down by half the margin rather than relaid out. Vertical only:
116
+ // shifting a child sideways without reflowing it would push a centred title
117
+ // off centre, which is the one arrangement that works here.
118
+ for (const auto &child : header->getChildren()) {
119
+ auto *inside = const_cast<ShadowNode *>(child.get());
120
+ auto *layoutable = dynamic_cast<LayoutableShadowNode *>(inside);
121
+ if (layoutable == nullptr) {
122
+ continue;
123
+ }
124
+ auto metrics = layoutable->getLayoutMetrics();
125
+ metrics.frame.origin.y += kHeaderPaddingY;
126
+ inside->ensureUnsealed();
127
+ layoutable->setLayoutMetrics(metrics);
128
+ }
129
+
130
+ for (const auto &child : getChildren()) {
131
+ auto *body = const_cast<ShadowNode *>(child.get());
132
+ if (body == static_cast<ShadowNode *>(header)) {
133
+ continue;
134
+ }
135
+ auto *layoutable = dynamic_cast<LayoutableShadowNode *>(body);
136
+ if (layoutable == nullptr) {
137
+ continue;
138
+ }
139
+ auto metrics = layoutable->getLayoutMetrics();
140
+ if (metrics.displayType == DisplayType::None || metrics.frame.origin.y != 0) {
141
+ continue;
142
+ }
143
+ metrics.frame.origin.y = barHeight;
144
+ metrics.frame.size.height = std::max(0.0f, metrics.frame.size.height - barHeight);
145
+ body->ensureUnsealed();
146
+ layoutable->setLayoutMetrics(metrics);
147
+ }
148
+ }
149
+
150
+ void RNSScreenStackShadowNode::layout(LayoutContext layoutContext) {
151
+ ConcreteViewShadowNode::layout(layoutContext);
152
+
153
+ const auto &children = getChildren();
154
+
155
+
156
+ // The topmost screen that is not see-through. Everything below it is covered
157
+ // and gets hidden; a transparent modal covers nothing, so the search keeps
158
+ // going down past one.
159
+ auto topOpaque = children.size();
160
+ for (auto i = children.size(); i-- > 0;) {
161
+ const auto *screen = dynamic_cast<const RNSScreenShadowNode *>(children[i].get());
162
+ if (screen == nullptr || screen->getConcreteProps().seeThrough) {
163
+ continue;
164
+ }
165
+ topOpaque = i;
166
+ break;
167
+ }
168
+ if (topOpaque == children.size()) {
169
+ return;
170
+ }
171
+
172
+ for (size_t i = 0; i < topOpaque; ++i) {
173
+ auto *child = const_cast<ShadowNode *>(children[i].get());
174
+ auto *layoutable = dynamic_cast<LayoutableShadowNode *>(child);
175
+ if (layoutable == nullptr) {
176
+ continue;
177
+ }
178
+ auto metrics = layoutable->getLayoutMetrics();
179
+ if (metrics.displayType == DisplayType::None) {
180
+ continue;
181
+ }
182
+ metrics.displayType = DisplayType::None;
183
+ // The same pair YogaLayoutableShadowNode::layout uses to write a child's
184
+ // metrics: a node is sealed after its commit and has to be told before it
185
+ // is written to.
186
+ child->ensureUnsealed();
187
+ layoutable->setLayoutMetrics(metrics);
188
+ }
189
+ }
190
+
191
+ } // namespace facebook::react
192
+
193
+ namespace basalt {
194
+
195
+ DesktopScreensModule::DesktopScreensModule(
196
+ std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
197
+ : TurboModule(kModuleName, std::move(jsInvoker)) {
198
+ // Deliberately empty. See the header.
199
+ }
200
+
201
+ } // namespace basalt
@@ -0,0 +1,216 @@
1
+ // react-native-screens, as much of it as a desktop needs.
2
+ //
3
+ // ## What this is for
4
+ //
5
+ // react-navigation's native stack renders every screen it has ever pushed and
6
+ // relies on the platform to show one of them. The signal is `activityState` on
7
+ // each `RNSScreen`: 0 for a screen that is behind another, 2 for the one in
8
+ // front. With no native half, every screen is mounted and laid out at full
9
+ // size, one painted over the other. It looks right and is not: both are in the
10
+ // tree, both are measured, and a screen reader reads both.
11
+ //
12
+ // Measured before this existed, pushing Details over Home left tags 18 and 38
13
+ // side by side in the dumped tree, both 900x700. See docs/backlog/ecosystem.md.
14
+ //
15
+ // ## Why `display: none` rather than a mounting peer
16
+ //
17
+ // The platforms react-native-screens was written for detach the inactive
18
+ // screen's view from the container, which is a mounting-layer operation and
19
+ // would be three implementations of it here. Yoga already has the concept:
20
+ // `display: none` takes a subtree out of layout entirely, and every host here
21
+ // already honours it, so an inactive screen stops being laid out and stops
22
+ // being painted without any host learning the word "screen".
23
+ //
24
+ // The difference from a real detach is that the shadow node still exists, which
25
+ // costs a node per background screen and nothing else. What it buys is that the
26
+ // behaviour is identical on all three desktops for free.
27
+ //
28
+ // ## What is deliberately not here
29
+ //
30
+ // The header. `RNSScreenStackHeaderConfig` carries a title, a back button and a
31
+ // tint, and drawing it means a real toolkit header per platform rather than a
32
+ // prop translation. It is registered as a plain view so that it mounts and
33
+ // takes no space, which is what it already did. An app that wants a title bar
34
+ // today draws its own; see docs/backlog/ecosystem.md.
35
+
36
+ #pragma once
37
+
38
+ #include <ReactCommon/TurboModule.h>
39
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
40
+ #include <react/renderer/components/view/ViewEventEmitter.h>
41
+ #include <react/renderer/components/view/ViewProps.h>
42
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
43
+ #include <react/renderer/core/LayoutContext.h>
44
+
45
+ namespace facebook::react {
46
+
47
+ extern const char RNSScreenComponentName[];
48
+
49
+ class RNSScreenProps final : public ViewProps {
50
+ public:
51
+ RNSScreenProps() = default;
52
+ RNSScreenProps(const PropsParserContext &context,
53
+ const RNSScreenProps &sourceProps,
54
+ const RawProps &rawProps);
55
+
56
+ // 0 inactive, 1 transitioning, 2 active, and -1 for "the library is not
57
+ // managing this one", which is its own default and has to stay visible.
58
+ float activityState{-1.0f};
59
+
60
+ // Whether the screen beneath this one shows through it. True for the
61
+ // `transparentModal` presentations and false for everything else, which is
62
+ // what decides how far down the stack hides. Parsed from `stackPresentation`
63
+ // rather than kept as the string, because that is the only question asked.
64
+ bool seeThrough{false};
65
+ };
66
+
67
+ // The screen, which also has to get its content out from under the header.
68
+ //
69
+ // react-native-screens gives the content wrapper `StyleSheet.absoluteFill`, so
70
+ // it covers the whole screen and the header, which is an ordinary in-flow
71
+ // child, is painted on top of it. That is right on a platform whose navigation
72
+ // bar floats; here it means the first 56 points of every screen are behind the
73
+ // bar.
74
+ //
75
+ // The screen is the only node that can see both, so it moves the content down
76
+ // by the header's height after layout. `adopt()` cannot do it instead: it runs
77
+ // before the children are attached, measured by a probe that saw zero of them.
78
+ class RNSScreenShadowNode final
79
+ : public ConcreteViewShadowNode<RNSScreenComponentName, RNSScreenProps,
80
+ ViewEventEmitter> {
81
+ public:
82
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
83
+
84
+ void layout(LayoutContext layoutContext) override;
85
+ };
86
+ using RNSScreenComponentDescriptor = ConcreteComponentDescriptor<RNSScreenShadowNode>;
87
+
88
+ // The containers. Each is a plain view that holds screens, so ViewProps parses
89
+ // all of it and the only thing each needs of its own is a name. A name Fabric
90
+ // does not know becomes UnimplementedNativeView, which mounts but carries none
91
+ // of the props, so registering them is what makes the stack lay out at all.
92
+ #define BASALT_SCREENS_VIEW(Symbol) \
93
+ extern const char Symbol##ComponentName[]; \
94
+ using Symbol##ShadowNode = \
95
+ ConcreteViewShadowNode<Symbol##ComponentName, ViewProps, ViewEventEmitter>; \
96
+ using Symbol##ComponentDescriptor = ConcreteComponentDescriptor<Symbol##ShadowNode>;
97
+
98
+ BASALT_SCREENS_VIEW(RNSScreenContainer)
99
+ BASALT_SCREENS_VIEW(RNSScreenNavigationContainer)
100
+ BASALT_SCREENS_VIEW(RNSScreenContentWrapper)
101
+ BASALT_SCREENS_VIEW(RNSScreenStackHeaderSubview)
102
+
103
+ #undef BASALT_SCREENS_VIEW
104
+
105
+ extern const char RNSScreenStackHeaderConfigComponentName[];
106
+
107
+ /**
108
+ * What a header puts around its content.
109
+ *
110
+ * A bar is its title plus a margin rather than a fixed height, which lands
111
+ * within a few points of the 56 every toolkit uses and centres a one-line
112
+ * title without needing to know how tall it is.
113
+ *
114
+ * Applied by the screen, not by the header, because the screen also moves the
115
+ * body down out of the way and the two have to happen together or not at all.
116
+ * See RNSScreenShadowNode::layout.
117
+ */
118
+ inline constexpr float kHeaderPaddingY = 18.0f;
119
+
120
+ // The header bar.
121
+ //
122
+ // On iOS and Android this component configures a navigation bar that the
123
+ // platform draws, and most of its props describe that bar. Here there is no
124
+ // such bar, and the useful half of it arrives as views: react-navigation's
125
+ // native stack takes its non-iOS branch on every desktop, which renders the
126
+ // title, the back button and anything custom as `RNSScreenStackHeaderSubview`
127
+ // children rather than as strings for a toolbar to draw.
128
+ //
129
+ // So this is a row that lays those children out, and the props it reads are the
130
+ // ones that describe the row rather than the ones that describe UIKit.
131
+ //
132
+ // `title` as a plain string is the part that does not arrive as a view, and is
133
+ // not drawn: react-navigation only renders it as a `<Text>` when the title is
134
+ // centred or supplied as a component. See the navigation page in the docs.
135
+ class RNSScreenStackHeaderConfigProps final : public ViewProps {
136
+ public:
137
+ RNSScreenStackHeaderConfigProps() = default;
138
+ RNSScreenStackHeaderConfigProps(const PropsParserContext &context,
139
+ const RNSScreenStackHeaderConfigProps &sourceProps,
140
+ const RawProps &rawProps);
141
+
142
+ bool hidden{false};
143
+ };
144
+
145
+ // A header with nothing in it takes no space.
146
+ //
147
+ // react-navigation renders the title as a view only when it is centred or
148
+ // supplied as a component; a plain left-aligned string goes to the `title`
149
+ // prop, for a toolbar that does not exist here, and the config ends up with no
150
+ // children at all. Reserving 56 points for that would replace today's missing
151
+ // header with an empty grey bar, which is worse: at least nothing looks
152
+ // deliberate.
153
+ //
154
+ // Decided after layout because that is the first point at which the children
155
+ // are attached. `adopt()` runs before they are, measured rather than assumed.
156
+ class RNSScreenStackHeaderConfigShadowNode final
157
+ : public ConcreteViewShadowNode<RNSScreenStackHeaderConfigComponentName,
158
+ RNSScreenStackHeaderConfigProps,
159
+ ViewEventEmitter> {
160
+ public:
161
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
162
+
163
+ void layout(LayoutContext layoutContext) override;
164
+ };
165
+ using RNSScreenStackHeaderConfigComponentDescriptor =
166
+ ConcreteComponentDescriptor<RNSScreenStackHeaderConfigShadowNode>;
167
+
168
+ extern const char RNSScreenStackComponentName[];
169
+
170
+ // The stack, which shows one screen and covers the rest.
171
+ //
172
+ // A native stack renders every screen it has pushed and expects the platform to
173
+ // show the top one. On iOS that is UINavigationController; `activityState` is
174
+ // not the signal, and measuring it is what established that: react-native-screens
175
+ // throws `activityState cannot be decreased in NativeStack`, and every screen in
176
+ // a pushed stack reports 2, the covered ones included.
177
+ //
178
+ // So the stack does it, after layout, by giving every screen below the topmost
179
+ // opaque one `DisplayType::None`. All three mounting managers already hide a
180
+ // view on exactly that, so this is one implementation rather than three, and a
181
+ // hidden widget is out of the accessibility tree on each of them for free.
182
+ //
183
+ // What this does not do is skip the layout. Yoga has already measured the
184
+ // covered screens by the time `layout()` can see which is on top, and the only
185
+ // way to get in front of that would be for a screen to know its own position in
186
+ // the stack, which it does not. The cost is measuring a subtree that is not
187
+ // shown; the thing that was actually wrong, two screens mounted and both
188
+ // reaching accessibility, is fixed.
189
+ class RNSScreenStackShadowNode final
190
+ : public ConcreteViewShadowNode<RNSScreenStackComponentName, ViewProps,
191
+ ViewEventEmitter> {
192
+ public:
193
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
194
+
195
+ void layout(LayoutContext layoutContext) override;
196
+ };
197
+
198
+
199
+ using RNSScreenStackComponentDescriptor =
200
+ ConcreteComponentDescriptor<RNSScreenStackShadowNode>;
201
+
202
+ } // namespace facebook::react
203
+
204
+ namespace basalt {
205
+
206
+ // `RNSModule`, whose TypeScript spec is `interface Spec extends TurboModule {}`:
207
+ // no methods at all. The library only asks whether it is there, and answering
208
+ // null is what produced the warning in every log. Nothing calls anything on it.
209
+ class DesktopScreensModule : public facebook::react::TurboModule {
210
+ public:
211
+ static constexpr auto kModuleName = "RNSModule";
212
+
213
+ explicit DesktopScreensModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
214
+ };
215
+
216
+ } // namespace basalt
@@ -15,6 +15,14 @@ ScrollRange scrollRangeFor(double containerLength,
15
15
  return range;
16
16
  }
17
17
 
18
+ ScrollAxisInsets centeringInsets(double containerLength, double contentLength) {
19
+ const double slack = containerLength - contentLength;
20
+ if (slack <= 0.0) {
21
+ return {};
22
+ }
23
+ return ScrollAxisInsets{.leading = slack / 2.0, .trailing = slack / 2.0};
24
+ }
25
+
18
26
  double clampScrollOffset(double offset,
19
27
  double containerLength,
20
28
  double contentLength,
@@ -54,6 +54,20 @@ ScrollRange scrollRangeFor(double containerLength,
54
54
  double contentLength,
55
55
  ScrollAxisInsets insets = {});
56
56
 
57
+ // `centerContent`: the inset that centres content smaller than its container.
58
+ //
59
+ // Half the slack at each end, and nothing at all once the content is longer
60
+ // than the container -- a list that can scroll has nothing to centre.
61
+ //
62
+ // An inset rather than a position, which is not a trick: with half the slack at
63
+ // each end the range above collapses to the single offset `-slack/2`, so the
64
+ // clamp every host already runs on every mutation puts the content in the
65
+ // middle and keeps it there. That is also how iOS does it --
66
+ // `RCTEnhancedScrollView`'s `centerContentIfNeeded` assigns a `contentInset` of
67
+ // exactly these numbers -- and why `centerContent` overrides the app's own
68
+ // `contentInset` there and here: the two are the same channel.
69
+ ScrollAxisInsets centeringInsets(double containerLength, double contentLength);
70
+
57
71
  // An offset brought inside that range.
58
72
  //
59
73
  // The one place the three hosts agreed already, and now the one place it is
@@ -38,6 +38,73 @@ inline constexpr double kScrollIndicatorInset = 2.0;
38
38
  // Below this a thumb stops reading as a position and starts reading as a dot.
39
39
  inline constexpr double kScrollIndicatorMinimumLength = 24.0;
40
40
 
41
+ // `indicatorStyle`, as React Native's three values.
42
+ //
43
+ // `UIScrollViewIndicatorStyleDefault` is "black with a white border", `.black`
44
+ // is black alone, and `.white` is the one a list over dark content asks for,
45
+ // where a black thumb is invisible. So the three are two fills and one border,
46
+ // and the border is what tells `default` from `black`: it is a hairline of
47
+ // white around the pill, which is what keeps the default thumb visible over
48
+ // dark and light content both.
49
+ enum class ScrollIndicatorStyle {
50
+ Default,
51
+ Black,
52
+ White,
53
+ };
54
+
55
+ // Straight sRGB with alpha, which all three toolkits take in that form.
56
+ struct ScrollIndicatorColour {
57
+ float red{0.0F};
58
+ float green{0.0F};
59
+ float blue{0.0F};
60
+ float alpha{0.0F};
61
+ };
62
+
63
+ // Translucent, so the thumb reads over whatever it sits on rather than hiding
64
+ // it. One number for both colours: a white thumb that was more opaque than the
65
+ // black one would read as a different control.
66
+ inline constexpr float kScrollIndicatorAlpha = 0.35F;
67
+
68
+ // `inline` rather than a function in the .cpp, and not for speed: the three
69
+ // view layers link no core at all -- each is built and tested with nothing but
70
+ // its toolkit, which is what lets the Win32 suite run on a box with a compiler
71
+ // and the SDK -- so anything they take from here has to be a header. The view
72
+ // layers need this one to spell their own default thumb, and the managers need
73
+ // it to answer the prop.
74
+ inline ScrollIndicatorColour scrollIndicatorColourFor(ScrollIndicatorStyle style) {
75
+ if (style == ScrollIndicatorStyle::White) {
76
+ return ScrollIndicatorColour{
77
+ .red = 1.0F, .green = 1.0F, .blue = 1.0F, .alpha = kScrollIndicatorAlpha};
78
+ }
79
+ // `Default` and `Black` share a fill and differ by the border below, which
80
+ // is exactly how iOS's two differ.
81
+ return ScrollIndicatorColour{
82
+ .red = 0.0F, .green = 0.0F, .blue = 0.0F, .alpha = kScrollIndicatorAlpha};
83
+ }
84
+
85
+ // A hairline, which is what a border around a six-point pill can be without
86
+ // becoming the thumb. One point rather than a fraction: two of the three hosts
87
+ // stroke in logical points and a half-point line lands differently in each.
88
+ inline constexpr double kScrollIndicatorBorderWidth = 1.0;
89
+
90
+ // Rather more opaque than the fill, for the one job it has: the fill is a wash
91
+ // that lets the content through, and an outline as faint as that would not
92
+ // separate a black thumb from black content behind it.
93
+ inline constexpr float kScrollIndicatorBorderAlpha = 0.6F;
94
+
95
+ // The border around the thumb, or nothing for the two styles that have none.
96
+ //
97
+ // Only `Default` has one. `black` and `white` are the styles an app chooses
98
+ // *because* it knows what is behind the thumb, and iOS draws neither with an
99
+ // outline; adding one to either would be this platform inventing a look.
100
+ inline ScrollIndicatorColour scrollIndicatorBorderColourFor(ScrollIndicatorStyle style) {
101
+ if (style == ScrollIndicatorStyle::Default) {
102
+ return ScrollIndicatorColour{
103
+ .red = 1.0F, .green = 1.0F, .blue = 1.0F, .alpha = kScrollIndicatorBorderAlpha};
104
+ }
105
+ return ScrollIndicatorColour{};
106
+ }
107
+
41
108
  struct ScrollIndicator {
42
109
  // False when everything fits: a scrollbar for content that cannot scroll is
43
110
  // noise, and every list shorter than its container would grow one.
@@ -62,6 +62,30 @@ bool ScrollMomentum::advance(double seconds, double &dx, double &dy) {
62
62
  return true;
63
63
  }
64
64
 
65
+ double scrollMomentumDistance(double velocity, double decelerationRate) {
66
+ const double speed = std::abs(velocity);
67
+ if (speed < kStopSpeed) {
68
+ // A release rather than a fling, which is the same threshold `start`
69
+ // refuses at: nothing coasts from here.
70
+ return 0.0;
71
+ }
72
+ const double rate = std::clamp(decelerationRate, 0.5, 0.9999);
73
+ const double decay = std::log(rate);
74
+ if (decay >= 0.0) {
75
+ return 0.0;
76
+ }
77
+
78
+ // Velocity is in pixels per second and the rate is a factor per millisecond,
79
+ // which is the awkwardness React Native's own prop carries. So the integral
80
+ // is over milliseconds and the velocity is divided by a thousand to match:
81
+ //
82
+ // distance = (v/1000) * integral of rate^t dt, from 0 to the moment the
83
+ // speed falls to kStopSpeed, which is (rate^T - 1) / ln(rate) with
84
+ // rate^T = kStopSpeed / |v|.
85
+ const double travelled = (speed / 1000.0) * ((kStopSpeed / speed) - 1.0) / decay;
86
+ return velocity < 0.0 ? -travelled : travelled;
87
+ }
88
+
65
89
  void ScrollMomentum::stop() {
66
90
  running_ = false;
67
91
  velocityX_ = 0;