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,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;
|