basalt-core 0.0.0-rc5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/basalt.d.ts.map +1 -1
- package/dist/cli/basalt.js +18 -1
- package/dist/cli/basalt.js.map +1 -1
- package/dist/cli/bundle-cli.js +0 -0
- package/dist/cli/packageApp.d.ts.map +1 -1
- package/dist/cli/packageApp.js +7 -1
- package/dist/cli/packageApp.js.map +1 -1
- package/dist/metro-config.d.ts.map +1 -1
- package/dist/metro-config.js +53 -5
- package/dist/metro-config.js.map +1 -1
- package/dist/src/KeyHandler.d.ts +1 -1
- package/dist/src/KeyHandler.d.ts.map +1 -1
- package/dist/src/KeyHandler.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/overrides/Alert.d.ts +1 -1
- package/dist/src/overrides/Alert.js +1 -1
- package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
- package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
- package/dist/src/overrides/BaseViewConfig.js +21 -3
- package/dist/src/overrides/BaseViewConfig.js.map +1 -1
- package/dist/src/overrides/Button.d.ts +81 -0
- package/dist/src/overrides/Button.d.ts.map +1 -0
- package/dist/src/overrides/Button.js +109 -0
- package/dist/src/overrides/Button.js.map +1 -0
- package/dist/src/overrides/ScrollView.d.ts +25 -2
- package/dist/src/overrides/ScrollView.d.ts.map +1 -1
- package/dist/src/overrides/ScrollView.js +64 -7
- package/dist/src/overrides/ScrollView.js.map +1 -1
- package/dist/src/overrides/Settings.d.ts +81 -0
- package/dist/src/overrides/Settings.d.ts.map +1 -0
- package/dist/src/overrides/Settings.js +120 -0
- package/dist/src/overrides/Settings.js.map +1 -0
- package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
- package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
- package/dist/src/overrides/processDecelerationRate.js +31 -0
- package/dist/src/overrides/processDecelerationRate.js.map +1 -0
- package/dist/src/useContextMenu.d.ts +57 -6
- package/dist/src/useContextMenu.d.ts.map +1 -1
- package/dist/src/useContextMenu.js +34 -1
- package/dist/src/useContextMenu.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/native/CMakeLists.txt +51 -0
- package/native/core/ActionSheet.cpp +75 -0
- package/native/core/ActionSheet.h +90 -0
- package/native/core/AppStateHost.cpp +76 -0
- package/native/core/AppStateHost.h +68 -0
- package/native/core/BackgroundLayers.h +282 -0
- package/native/core/BlendModes.h +67 -0
- package/native/core/CoreModules.cpp +380 -11
- package/native/core/CoreModules.h +140 -4
- package/native/core/CrashHandler.cpp +246 -0
- package/native/core/CrashHandler.h +65 -0
- package/native/core/CursorNames.h +110 -0
- package/native/core/DevMenu.cpp +6 -5
- package/native/core/DeviceMetrics.cpp +87 -0
- package/native/core/DeviceMetrics.h +80 -0
- package/native/core/Filters.h +336 -0
- package/native/core/FontFitting.h +183 -0
- package/native/core/FontScaling.h +153 -0
- package/native/core/FontVariants.h +148 -0
- package/native/core/Gradients.h +523 -0
- package/native/core/ImageAnimation.h +109 -0
- package/native/core/ImageBytes.cpp +28 -1
- package/native/core/ImageBytes.h +17 -1
- package/native/core/ImageProgress.h +85 -0
- package/native/core/LabelRegistry.h +189 -0
- package/native/core/LegacyShadow.h +90 -0
- package/native/core/LiveRegions.h +114 -0
- package/native/core/MenuModule.cpp +35 -4
- package/native/core/MountingWalk.h +125 -2
- package/native/core/PlatformServices.h +187 -3
- package/native/core/SafeAreaComponent.cpp +115 -0
- package/native/core/SafeAreaComponent.h +105 -0
- package/native/core/ScreensComponent.cpp +201 -0
- package/native/core/ScreensComponent.h +216 -0
- package/native/core/ScrollBounds.cpp +8 -0
- package/native/core/ScrollBounds.h +14 -0
- package/native/core/ScrollIndicator.h +67 -0
- package/native/core/ScrollMomentum.cpp +24 -0
- package/native/core/ScrollMomentum.h +14 -0
- package/native/core/ScrollSnap.cpp +121 -12
- package/native/core/ScrollSnap.h +45 -0
- package/native/core/ScrollVisiblePosition.cpp +68 -0
- package/native/core/ScrollVisiblePosition.h +101 -0
- package/native/core/SettingsStore.cpp +199 -0
- package/native/core/SettingsStore.h +127 -0
- package/native/core/ShadowShape.h +38 -0
- package/native/core/SystemLocale.cpp +103 -0
- package/native/core/SystemLocale.h +86 -0
- package/native/core/TestDialog.cpp +45 -6
- package/native/core/TestDialog.h +15 -0
- package/native/core/TextAlignments.h +240 -0
- package/native/core/TextChecking.h +133 -0
- package/native/core/TextColors.h +83 -0
- package/native/core/TextDecorations.h +101 -0
- package/native/core/TextDirection.cpp +242 -0
- package/native/core/TextDirection.h +88 -0
- package/native/core/TextHighlight.h +30 -0
- package/native/core/TextSelection.cpp +97 -0
- package/native/core/TextSelection.h +161 -0
- package/native/core/TextShadows.h +96 -0
- package/native/core/TextVerticalAlign.h +33 -0
- package/native/core/UIManagerAccess.cpp +36 -0
- package/native/core/UIManagerAccess.h +28 -0
- package/native/core/WindowBoundsCache.cpp +58 -1
- package/native/core/WindowControl.h +24 -0
- package/native/core/WritingDirections.h +52 -0
- package/native/core/portability_probe.cpp +7 -0
- package/native/tests/RenamedFontFile.h +161 -0
- package/native/tests/TestHarness.cpp +18 -0
- package/native/tests/test_action_sheet.cpp +157 -0
- package/native/tests/test_app_state.cpp +60 -0
- package/native/tests/test_background_layers.cpp +281 -0
- package/native/tests/test_blend_modes.cpp +79 -0
- package/native/tests/test_blobs.cpp +1 -1
- package/native/tests/test_device_metrics.cpp +126 -0
- package/native/tests/test_filters.cpp +271 -0
- package/native/tests/test_font_fitting.cpp +172 -0
- package/native/tests/test_font_scaling.cpp +127 -0
- package/native/tests/test_font_variants.cpp +84 -0
- package/native/tests/test_gradients.cpp +390 -0
- package/native/tests/test_image_animation.cpp +105 -0
- package/native/tests/test_image_progress.cpp +94 -0
- package/native/tests/test_label_registry.cpp +262 -0
- package/native/tests/test_legacy_shadow.cpp +165 -0
- package/native/tests/test_live_regions.cpp +108 -0
- package/native/tests/test_menu_entries.cpp +192 -0
- package/native/tests/test_momentum.cpp +45 -0
- package/native/tests/test_scroll_bounds.cpp +42 -0
- package/native/tests/test_scroll_indicator.cpp +61 -0
- package/native/tests/test_scroll_snap.cpp +129 -0
- package/native/tests/test_scroll_visible_position.cpp +121 -0
- package/native/tests/test_settings_store.cpp +257 -0
- package/native/tests/test_shadow_shape.cpp +47 -0
- package/native/tests/test_system_locale.cpp +36 -0
- package/native/tests/test_text_alignment.cpp +299 -0
- package/native/tests/test_text_checking.cpp +31 -0
- package/native/tests/test_text_colors.cpp +92 -0
- package/native/tests/test_text_decorations.cpp +100 -0
- package/native/tests/test_text_selection.cpp +168 -0
- package/native/tests/test_text_shadows.cpp +132 -0
- package/package.json +4 -2
|
@@ -70,4 +70,18 @@ class ScrollMomentum {
|
|
|
70
70
|
double decelerationRate_{kNormalDeceleration};
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
// How far a fling at `velocity` would travel before it stopped, signed like
|
|
74
|
+
// the velocity.
|
|
75
|
+
//
|
|
76
|
+
// The closed form of the decay `advance` steps through, and it exists for
|
|
77
|
+
// `core/ScrollSnap.h`: a snapping list has to know where momentum *would* have
|
|
78
|
+
// carried it before it can decide which point to settle on, which is what
|
|
79
|
+
// `disableIntervalMomentum` turns off. Nothing animates from this -- the fling
|
|
80
|
+
// above is still what moves a list that is not snapping.
|
|
81
|
+
//
|
|
82
|
+
// Integrated to the speed the fling stops at rather than to zero, because that
|
|
83
|
+
// is where `advance` gives up: the tail below it is never travelled, and a
|
|
84
|
+
// projection that included it would reach past every real fling.
|
|
85
|
+
double scrollMomentumDistance(double velocity, double decelerationRate);
|
|
86
|
+
|
|
73
87
|
} // namespace basalt
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
#include "ScrollSnap.h"
|
|
2
2
|
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
#include "ScrollMomentum.h"
|
|
6
|
+
|
|
3
7
|
#include <algorithm>
|
|
4
8
|
#include <cmath>
|
|
9
|
+
#include <cstdio>
|
|
5
10
|
|
|
6
11
|
namespace basalt {
|
|
7
12
|
|
|
8
13
|
namespace {
|
|
9
14
|
|
|
15
|
+
// `%g` without a trailing `.000000`, which is what every other number in a tree
|
|
16
|
+
// dump is printed with.
|
|
17
|
+
std::string shortNumber(double value) {
|
|
18
|
+
char text[32];
|
|
19
|
+
std::snprintf(text, sizeof(text), "%g", value);
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
// What the alignment shifts by: a centred snap lines the middle of the
|
|
11
24
|
// container up with a point, an end-aligned one lines up its trailing edge.
|
|
12
25
|
double alignmentShift(ScrollSnapAlignment alignment, double containerLength) {
|
|
@@ -27,19 +40,54 @@ double clampToContent(double offset, double containerLength, double contentLengt
|
|
|
27
40
|
}
|
|
28
41
|
|
|
29
42
|
// The nearest of an explicit list, with a flick moving at least one entry.
|
|
30
|
-
|
|
43
|
+
//
|
|
44
|
+
// `landing` is where the fling would have come to rest and `offset` is where
|
|
45
|
+
// the finger left; for a release the two are the same. The candidates are the
|
|
46
|
+
// listed offsets *and* the content's own two edges, which is what
|
|
47
|
+
// `snapToStart` and `snapToEnd` are about -- "by default the beginning of the
|
|
48
|
+
// list counts as a snap offset", in upstream's words, and those two props turn
|
|
49
|
+
// the default off.
|
|
50
|
+
std::optional<double> nearestOffset(const ScrollSnapConfig &config,
|
|
31
51
|
double offset,
|
|
32
|
-
double
|
|
33
|
-
|
|
52
|
+
double landing,
|
|
53
|
+
double velocity,
|
|
54
|
+
double furthest) {
|
|
55
|
+
if (config.offsets.empty()) {
|
|
34
56
|
return std::nullopt;
|
|
35
57
|
}
|
|
36
|
-
std::vector<double> sorted = offsets;
|
|
58
|
+
std::vector<double> sorted = config.offsets;
|
|
59
|
+
std::sort(sorted.begin(), sorted.end());
|
|
60
|
+
const double first = sorted.front();
|
|
61
|
+
const double last = sorted.back();
|
|
62
|
+
|
|
63
|
+
// Free scrolling between an edge and the listed point nearest it, when the
|
|
64
|
+
// app asked for that. Upstream's own two conditions, and both halves matter:
|
|
65
|
+
// a list already past the last point coasts, and one that is *not* yet past
|
|
66
|
+
// it still settles there rather than sailing into the gap.
|
|
67
|
+
if (!config.snapToEnd && landing >= last) {
|
|
68
|
+
if (offset >= last) {
|
|
69
|
+
return std::nullopt;
|
|
70
|
+
}
|
|
71
|
+
return last;
|
|
72
|
+
}
|
|
73
|
+
if (!config.snapToStart && landing <= first) {
|
|
74
|
+
if (offset <= first) {
|
|
75
|
+
return std::nullopt;
|
|
76
|
+
}
|
|
77
|
+
return first;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The content's edges as candidates, which is the default behaviour the two
|
|
81
|
+
// props above switch off. Inserted rather than special-cased so that
|
|
82
|
+
// "nearest" and "next in that direction" both see them.
|
|
83
|
+
sorted.insert(sorted.begin(), 0.0);
|
|
84
|
+
sorted.push_back(furthest);
|
|
37
85
|
std::sort(sorted.begin(), sorted.end());
|
|
38
86
|
|
|
39
87
|
auto nearest = sorted.begin();
|
|
40
|
-
double best = std::abs(sorted.front() -
|
|
88
|
+
double best = std::abs(sorted.front() - landing);
|
|
41
89
|
for (auto it = sorted.begin(); it != sorted.end(); ++it) {
|
|
42
|
-
const double distance = std::abs(*it -
|
|
90
|
+
const double distance = std::abs(*it - landing);
|
|
43
91
|
if (distance < best) {
|
|
44
92
|
best = distance;
|
|
45
93
|
nearest = it;
|
|
@@ -47,10 +95,12 @@ std::optional<double> nearestOffset(const std::vector<double> &offsets,
|
|
|
47
95
|
}
|
|
48
96
|
|
|
49
97
|
if (velocity > kScrollSnapFlickVelocity) {
|
|
50
|
-
// The first entry
|
|
51
|
-
// settle back on the point it started from
|
|
98
|
+
// The first entry at or past where the fling would have landed. A flick
|
|
99
|
+
// forwards must not settle back on the point it started from, which is
|
|
100
|
+
// what the `> offset` rather than `>= landing` guard is for when the two
|
|
101
|
+
// are the same.
|
|
52
102
|
for (auto it = sorted.begin(); it != sorted.end(); ++it) {
|
|
53
|
-
if (*it > offset) {
|
|
103
|
+
if (*it >= landing && *it > offset) {
|
|
54
104
|
return *it;
|
|
55
105
|
}
|
|
56
106
|
}
|
|
@@ -58,7 +108,7 @@ std::optional<double> nearestOffset(const std::vector<double> &offsets,
|
|
|
58
108
|
}
|
|
59
109
|
if (velocity < -kScrollSnapFlickVelocity) {
|
|
60
110
|
for (auto it = sorted.rbegin(); it != sorted.rend(); ++it) {
|
|
61
|
-
if (*it < offset) {
|
|
111
|
+
if (*it <= landing && *it < offset) {
|
|
62
112
|
return *it;
|
|
63
113
|
}
|
|
64
114
|
}
|
|
@@ -69,6 +119,55 @@ std::optional<double> nearestOffset(const std::vector<double> &offsets,
|
|
|
69
119
|
|
|
70
120
|
} // namespace
|
|
71
121
|
|
|
122
|
+
std::string describeScrollSnap(const ScrollSnapConfig &config) {
|
|
123
|
+
std::string out;
|
|
124
|
+
if (config.enabled()) {
|
|
125
|
+
out += " snap=";
|
|
126
|
+
if (config.paging) {
|
|
127
|
+
// The container's own length, which only the host knows; naming the mode
|
|
128
|
+
// is the whole of what this line can say.
|
|
129
|
+
out += "paging";
|
|
130
|
+
} else if (!config.offsets.empty()) {
|
|
131
|
+
out += "offsets:" + std::to_string(config.offsets.size());
|
|
132
|
+
} else {
|
|
133
|
+
out += "interval:" + shortNumber(config.interval);
|
|
134
|
+
}
|
|
135
|
+
switch (config.alignment) {
|
|
136
|
+
case ScrollSnapAlignment::Center:
|
|
137
|
+
out += ",align=center";
|
|
138
|
+
break;
|
|
139
|
+
case ScrollSnapAlignment::End:
|
|
140
|
+
out += ",align=end";
|
|
141
|
+
break;
|
|
142
|
+
case ScrollSnapAlignment::Start:
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
// Printed only when false, both of them: true is the default and every
|
|
146
|
+
// scroll view would otherwise carry two words that say nothing.
|
|
147
|
+
if (!config.snapToStart) {
|
|
148
|
+
out += ",start=off";
|
|
149
|
+
}
|
|
150
|
+
if (!config.snapToEnd) {
|
|
151
|
+
out += ",end=off";
|
|
152
|
+
}
|
|
153
|
+
if (config.disableIntervalMomentum) {
|
|
154
|
+
out += ",momentum=off";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Beside the snapping because that is what it is for here: the rate decides
|
|
158
|
+
// where a fling would have landed, which is the point a snap is chosen from.
|
|
159
|
+
// Only when it is not React Native's own default, so an ordinary list says
|
|
160
|
+
// nothing.
|
|
161
|
+
//
|
|
162
|
+
// Compared with a tolerance rather than exactly: the prop is a `float` in
|
|
163
|
+
// ReactCommon, so the default arrives here as 0.99800002574920654 and an
|
|
164
|
+
// equality test would print a line for every scroll view in every app.
|
|
165
|
+
if (std::abs(config.decelerationRate - ScrollMomentum::kNormalDeceleration) > 1e-6) {
|
|
166
|
+
out += " decel=" + shortNumber(config.decelerationRate);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
72
171
|
std::optional<double> scrollSnapTarget(const ScrollSnapConfig &config,
|
|
73
172
|
double offset,
|
|
74
173
|
double velocity,
|
|
@@ -78,11 +177,21 @@ std::optional<double> scrollSnapTarget(const ScrollSnapConfig &config,
|
|
|
78
177
|
return std::nullopt;
|
|
79
178
|
}
|
|
80
179
|
|
|
180
|
+
// Where the fling would have come to rest, which is the offset a snap point
|
|
181
|
+
// is chosen next to unless the app asked for the paged behaviour. Zero for a
|
|
182
|
+
// release, for `pagingEnabled` -- one page per flick, whatever it was worth --
|
|
183
|
+
// and for `disableIntervalMomentum`, which is the prop for asking that a
|
|
184
|
+
// spacing behave like a page.
|
|
185
|
+
const double landing = config.paging || config.disableIntervalMomentum
|
|
186
|
+
? offset
|
|
187
|
+
: offset + scrollMomentumDistance(velocity, config.decelerationRate);
|
|
188
|
+
|
|
81
189
|
// An explicit list wins over a spacing, and a spacing over paging only when
|
|
82
190
|
// paging is off: `pagingEnabled` is the coarsest statement and iOS lets it
|
|
83
191
|
// take precedence.
|
|
84
192
|
if (!config.paging && !config.offsets.empty()) {
|
|
85
|
-
const auto target = nearestOffset(
|
|
193
|
+
const auto target = nearestOffset(
|
|
194
|
+
config, offset, landing, velocity, std::max(0.0, contentLength - containerLength));
|
|
86
195
|
if (!target) {
|
|
87
196
|
return std::nullopt;
|
|
88
197
|
}
|
|
@@ -98,7 +207,7 @@ std::optional<double> scrollSnapTarget(const ScrollSnapConfig &config,
|
|
|
98
207
|
// up with a multiple is the container's leading edge, its middle, or its
|
|
99
208
|
// trailing edge.
|
|
100
209
|
const double shift = alignmentShift(config.alignment, containerLength);
|
|
101
|
-
const double aligned =
|
|
210
|
+
const double aligned = landing + shift;
|
|
102
211
|
|
|
103
212
|
double index = std::floor(aligned / interval);
|
|
104
213
|
const double within = aligned - index * interval;
|
package/native/core/ScrollSnap.h
CHANGED
|
@@ -29,10 +29,29 @@
|
|
|
29
29
|
//
|
|
30
30
|
// Below that threshold the nearest point wins, which is the behaviour of
|
|
31
31
|
// letting go without meaning anything by it.
|
|
32
|
+
//
|
|
33
|
+
// ## Next to *where*, which is the part `disableIntervalMomentum` is about
|
|
34
|
+
//
|
|
35
|
+
// For `pagingEnabled` the answer is the next page and nothing further, however
|
|
36
|
+
// hard the list was thrown: that is what a paged view does on iOS, where
|
|
37
|
+
// UIScrollView turns one page per flick.
|
|
38
|
+
//
|
|
39
|
+
// `snapToInterval` and `snapToOffsets` are not that. There the momentum is
|
|
40
|
+
// real -- a hard throw crosses several points and settles at the one it
|
|
41
|
+
// arrives near -- so the point is chosen from where the fling *would have
|
|
42
|
+
// landed*, projected with the same friction model `core/ScrollMomentum.h`
|
|
43
|
+
// animates. `disableIntervalMomentum` is the prop that asks for the other
|
|
44
|
+
// behaviour, and gives the paged one: the next point from where the finger
|
|
45
|
+
// left.
|
|
46
|
+
//
|
|
47
|
+
// Upstream decides this the same way, in `RCTEnhancedScrollView`'s
|
|
48
|
+
// `scrollViewWillEndDragging`, except that it is handed iOS's own predicted
|
|
49
|
+
// target rather than projecting one.
|
|
32
50
|
|
|
33
51
|
#pragma once
|
|
34
52
|
|
|
35
53
|
#include <optional>
|
|
54
|
+
#include <string>
|
|
36
55
|
#include <vector>
|
|
37
56
|
|
|
38
57
|
namespace basalt {
|
|
@@ -56,6 +75,22 @@ struct ScrollSnapConfig {
|
|
|
56
75
|
// spacing.
|
|
57
76
|
std::vector<double> offsets;
|
|
58
77
|
ScrollSnapAlignment alignment{ScrollSnapAlignment::Start};
|
|
78
|
+
// `snapToStart` and `snapToEnd`, which are about `snapToOffsets` and nothing
|
|
79
|
+
// else: by default the content's own start and end count as snap points
|
|
80
|
+
// beside the listed ones, and `false` lets the list scroll freely between an
|
|
81
|
+
// edge and the nearest listed point. Both default to true, as upstream's
|
|
82
|
+
// props and `ScrollView.js` do.
|
|
83
|
+
bool snapToStart{true};
|
|
84
|
+
bool snapToEnd{true};
|
|
85
|
+
// `disableIntervalMomentum`: a fling stops at the point next to where the
|
|
86
|
+
// *finger* left rather than where the coasting would have carried it. For a
|
|
87
|
+
// page less wide than the list, which is what React Native's own
|
|
88
|
+
// documentation says it is for.
|
|
89
|
+
bool disableIntervalMomentum{false};
|
|
90
|
+
// `decelerationRate`, needed to know where a fling would have landed. The
|
|
91
|
+
// same per-millisecond factor `core/ScrollMomentum.h` takes, defaulting to
|
|
92
|
+
// React Native's `'normal'`; a host that reads the prop passes its own.
|
|
93
|
+
double decelerationRate{0.998};
|
|
59
94
|
|
|
60
95
|
bool enabled() const {
|
|
61
96
|
return paging || interval > 0.0 || !offsets.empty();
|
|
@@ -67,6 +102,16 @@ struct ScrollSnapConfig {
|
|
|
67
102
|
// the wobble of letting go does not.
|
|
68
103
|
inline constexpr double kScrollSnapFlickVelocity = 250.0;
|
|
69
104
|
|
|
105
|
+
// What a scroll view's snapping is set to, as a tree dump prints it: a
|
|
106
|
+
// fragment like ` snap=interval:420,momentum=off decel=0.99`, or empty when
|
|
107
|
+
// there is nothing to say.
|
|
108
|
+
//
|
|
109
|
+
// Here rather than in each host so that the three dumps cannot drift, which is
|
|
110
|
+
// the same argument `core/TextAlignments.h` makes for the names it resolves.
|
|
111
|
+
// It exists because none of this is visible any other way: a snap point changes
|
|
112
|
+
// where a fling *ends*, and a tree dump is taken when nothing is moving.
|
|
113
|
+
std::string describeScrollSnap(const ScrollSnapConfig &config);
|
|
114
|
+
|
|
70
115
|
// The offset to settle at, or nothing when no snapping is configured.
|
|
71
116
|
//
|
|
72
117
|
// `velocity` is in pixels per second, positive meaning the content offset is
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#include "ScrollVisiblePosition.h"
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
|
|
5
|
+
namespace basalt {
|
|
6
|
+
|
|
7
|
+
namespace {
|
|
8
|
+
|
|
9
|
+
// Below this an adjustment is rounding rather than movement. Upstream's number,
|
|
10
|
+
// and the reason for having one at all is that layout arithmetic repeats to
|
|
11
|
+
// within a fraction of a point: a list that moved by that on every transaction
|
|
12
|
+
// would drift under the person reading it.
|
|
13
|
+
constexpr double kMinimumMove = 0.5;
|
|
14
|
+
|
|
15
|
+
} // namespace
|
|
16
|
+
|
|
17
|
+
ScrollPinnedChild firstVisibleChild(const std::vector<ScrollChildBox> &children,
|
|
18
|
+
int minIndexForVisible,
|
|
19
|
+
double offset) {
|
|
20
|
+
if (children.empty()) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
const auto first = static_cast<size_t>(minIndexForVisible < 0 ? 0 : minIndexForVisible);
|
|
24
|
+
if (first >= children.size()) {
|
|
25
|
+
// A `minIndexForVisible` past the end of the list, which a list that has
|
|
26
|
+
// not rendered its rows yet produces. Nothing to watch rather than the last
|
|
27
|
+
// child: the index is the app saying which children it does not want
|
|
28
|
+
// watched.
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (size_t index = first; index < children.size(); index++) {
|
|
33
|
+
const ScrollChildBox &child = children[index];
|
|
34
|
+
const bool visible = child.leading + child.length > offset;
|
|
35
|
+
// The last child is watched whether or not it is visible: a list scrolled
|
|
36
|
+
// past everything has no visible child, and watching nothing would mean
|
|
37
|
+
// adjusting by nothing.
|
|
38
|
+
if (visible || index + 1 == children.size()) {
|
|
39
|
+
return ScrollPinnedChild{.has = true, .tag = child.tag, .leading = child.leading};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ScrollVisibleAdjustment visiblePositionAdjustment(
|
|
46
|
+
double previousLeading,
|
|
47
|
+
double currentLeading,
|
|
48
|
+
double offset,
|
|
49
|
+
std::optional<int> autoscrollToStartThreshold) {
|
|
50
|
+
const double delta = currentLeading - previousLeading;
|
|
51
|
+
if (std::abs(delta) <= kMinimumMove) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ScrollVisibleAdjustment adjustment;
|
|
56
|
+
adjustment.move = true;
|
|
57
|
+
adjustment.delta = delta;
|
|
58
|
+
// Measured against the offset as it stands, before the adjustment: the
|
|
59
|
+
// question is where the person was looking, not where they are about to be
|
|
60
|
+
// put. Upstream reads the offset into a local for exactly that reason.
|
|
61
|
+
if (autoscrollToStartThreshold.has_value()
|
|
62
|
+
&& offset <= static_cast<double>(*autoscrollToStartThreshold)) {
|
|
63
|
+
adjustment.autoscrollToStart = true;
|
|
64
|
+
}
|
|
65
|
+
return adjustment;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
} // namespace basalt
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// `maintainVisibleContentPosition`: keeping the content still while items are
|
|
2
|
+
// added above it.
|
|
3
|
+
//
|
|
4
|
+
// A chat list is the case the prop exists for. Messages arrive at the top of a
|
|
5
|
+
// list that is scrolled to the bottom, or older ones are prepended when the
|
|
6
|
+
// person pulls up, and without this the content under their eyes jumps by the
|
|
7
|
+
// height of whatever was inserted. React Native's answer is to measure one
|
|
8
|
+
// child before the mutation and again after, and to move the scroll offset by
|
|
9
|
+
// however far that child moved -- so the child stays where it was and the
|
|
10
|
+
// insertion happens off screen.
|
|
11
|
+
//
|
|
12
|
+
// ## Why this is arithmetic and not a host's business
|
|
13
|
+
//
|
|
14
|
+
// The whole of it is: which child to watch, how far it moved, and whether the
|
|
15
|
+
// list was near enough to the start to be taken there instead. None of that
|
|
16
|
+
// needs a toolkit, and all three hosts would otherwise spell it themselves --
|
|
17
|
+
// which is how the three scroll ranges and three snapping rules started, before
|
|
18
|
+
// `core/ScrollBounds.h` and `core/ScrollSnap.h`.
|
|
19
|
+
//
|
|
20
|
+
// What a host does provide is the children's boxes before and after, which is
|
|
21
|
+
// the one part a toolkit knows and this does not.
|
|
22
|
+
//
|
|
23
|
+
// ## The rules, which are upstream's
|
|
24
|
+
//
|
|
25
|
+
// `RCTScrollViewComponentView` does this in two halves, around the mounting
|
|
26
|
+
// transaction: `_prepareForMaintainVisibleScrollPosition` before and
|
|
27
|
+
// `_adjustForMaintainVisibleContentPosition` after. The three rules worth
|
|
28
|
+
// naming, each of which has a test:
|
|
29
|
+
//
|
|
30
|
+
// - **The child watched is the first one that is even partly visible**, from
|
|
31
|
+
// `minIndexForVisible` onwards: the first whose trailing edge is past the
|
|
32
|
+
// offset. A list may keep a header out of this with that index, which is
|
|
33
|
+
// what it is for.
|
|
34
|
+
// - **The last child is the fallback.** When everything is scrolled past,
|
|
35
|
+
// there is no visible child to watch and the last one is watched anyway;
|
|
36
|
+
// without that a list scrolled to its end would adjust by nothing.
|
|
37
|
+
// - **Half a point is not a move.** Layout arithmetic repeats to within a
|
|
38
|
+
// rounding error, and an adjustment of a fraction of a pixel on every
|
|
39
|
+
// transaction would be a list that drifts.
|
|
40
|
+
//
|
|
41
|
+
// And `autoscrollToTopThreshold`, which is the opposite behaviour and reads
|
|
42
|
+
// oddly until the case is named: a chat list sitting *at* the top, where new
|
|
43
|
+
// messages arrive, should follow them rather than hold still. So a list within
|
|
44
|
+
// that many points of the start is taken to the start instead.
|
|
45
|
+
|
|
46
|
+
#pragma once
|
|
47
|
+
|
|
48
|
+
#include <optional>
|
|
49
|
+
#include <vector>
|
|
50
|
+
|
|
51
|
+
namespace basalt {
|
|
52
|
+
|
|
53
|
+
// One child of the content view, on the axis that scrolls. `leading` is its
|
|
54
|
+
// top (or left) in the content's own coordinates and `length` its height (or
|
|
55
|
+
// width).
|
|
56
|
+
//
|
|
57
|
+
// The tag travels with it because the child watched has to be found again after
|
|
58
|
+
// the mutation, and its position in the list is exactly what may have changed.
|
|
59
|
+
struct ScrollChildBox {
|
|
60
|
+
int tag{0};
|
|
61
|
+
double leading{0.0};
|
|
62
|
+
double length{0.0};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// The child a scroll view is watching, remembered across a transaction.
|
|
66
|
+
struct ScrollPinnedChild {
|
|
67
|
+
bool has{false};
|
|
68
|
+
int tag{0};
|
|
69
|
+
double leading{0.0};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Which child to watch, given the children before the mutation.
|
|
73
|
+
//
|
|
74
|
+
// Empty when there are no children at or after `minIndexForVisible`, which is
|
|
75
|
+
// also what an empty list gives: there is nothing to keep still.
|
|
76
|
+
ScrollPinnedChild firstVisibleChild(const std::vector<ScrollChildBox> &children,
|
|
77
|
+
int minIndexForVisible,
|
|
78
|
+
double offset);
|
|
79
|
+
|
|
80
|
+
// What to do once the mutation has happened and the watched child has moved.
|
|
81
|
+
struct ScrollVisibleAdjustment {
|
|
82
|
+
// False when nothing moved far enough to be worth moving the list for.
|
|
83
|
+
bool move{false};
|
|
84
|
+
// How far the offset should change, signed the same way the offset is.
|
|
85
|
+
double delta{0.0};
|
|
86
|
+
// `autoscrollToTopThreshold`: the list was near the start, so it follows the
|
|
87
|
+
// new content to the start rather than holding its place. The caller animates
|
|
88
|
+
// this one, where the adjustment above is instant -- which is upstream's
|
|
89
|
+
// division too.
|
|
90
|
+
bool autoscrollToStart{false};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// `previousLeading` is where the watched child was before the mutation and
|
|
94
|
+
// `currentLeading` where it is now; `offset` is the scroll offset as it stands,
|
|
95
|
+
// which is the one the threshold is measured against.
|
|
96
|
+
ScrollVisibleAdjustment visiblePositionAdjustment(double previousLeading,
|
|
97
|
+
double currentLeading,
|
|
98
|
+
double offset,
|
|
99
|
+
std::optional<int> autoscrollToStartThreshold);
|
|
100
|
+
|
|
101
|
+
} // namespace basalt
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#include "SettingsStore.h"
|
|
2
|
+
|
|
3
|
+
#include "AppIdentity.h"
|
|
4
|
+
|
|
5
|
+
#include <folly/json.h>
|
|
6
|
+
|
|
7
|
+
#include <glog/logging.h>
|
|
8
|
+
|
|
9
|
+
#include <cstdlib>
|
|
10
|
+
#include <fstream>
|
|
11
|
+
#include <sstream>
|
|
12
|
+
#include <system_error>
|
|
13
|
+
#include <utility>
|
|
14
|
+
|
|
15
|
+
namespace basalt {
|
|
16
|
+
|
|
17
|
+
namespace {
|
|
18
|
+
|
|
19
|
+
std::string environmentValue(const char *name) {
|
|
20
|
+
const char *value = std::getenv(name);
|
|
21
|
+
return value == nullptr ? std::string{} : std::string(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The per-user configuration directory, by each desktop's own rule.
|
|
25
|
+
//
|
|
26
|
+
// Read from the environment rather than asked of an API on purpose: the
|
|
27
|
+
// authoritative calls are `g_get_user_config_dir`, `-[NSFileManager
|
|
28
|
+
// URLsForDirectory:inDomains:]` and `SHGetKnownFolderPath`, which are three
|
|
29
|
+
// libraries this file does not link and which all three answer from the same
|
|
30
|
+
// variables anyway. A host that wants the real call can pass a path through
|
|
31
|
+
// `BASALT_SETTINGS_FILE`.
|
|
32
|
+
std::filesystem::path userConfigDirectory() {
|
|
33
|
+
#if defined(_WIN32)
|
|
34
|
+
const std::string appData = environmentValue("APPDATA");
|
|
35
|
+
if (!appData.empty()) {
|
|
36
|
+
return std::filesystem::path(appData);
|
|
37
|
+
}
|
|
38
|
+
const std::string profile = environmentValue("USERPROFILE");
|
|
39
|
+
if (profile.empty()) {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
return std::filesystem::path(profile) / "AppData" / "Roaming";
|
|
43
|
+
#else
|
|
44
|
+
const std::string home = environmentValue("HOME");
|
|
45
|
+
#if defined(__APPLE__)
|
|
46
|
+
// Where a macOS application's own files go. `~/Library/Preferences` is where
|
|
47
|
+
// `NSUserDefaults` writes, and writing a file of our own into it would put
|
|
48
|
+
// something that is not a plist where `defaults` expects one.
|
|
49
|
+
if (home.empty()) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
return std::filesystem::path(home) / "Library" / "Application Support";
|
|
53
|
+
#else
|
|
54
|
+
const std::string configHome = environmentValue("XDG_CONFIG_HOME");
|
|
55
|
+
if (!configHome.empty()) {
|
|
56
|
+
return std::filesystem::path(configHome);
|
|
57
|
+
}
|
|
58
|
+
if (home.empty()) {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
return std::filesystem::path(home) / ".config";
|
|
62
|
+
#endif
|
|
63
|
+
#endif
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
} // namespace
|
|
67
|
+
|
|
68
|
+
std::filesystem::path settingsFilePath() {
|
|
69
|
+
const std::string chosen = environmentValue(kSettingsFileVar);
|
|
70
|
+
if (!chosen.empty()) {
|
|
71
|
+
return std::filesystem::path(chosen);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const std::filesystem::path directory = userConfigDirectory();
|
|
75
|
+
if (directory.empty()) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const AppIdentity &identity = appIdentity();
|
|
80
|
+
return directory / (identity.identifier.empty() ? std::string("basalt") : identity.identifier) /
|
|
81
|
+
"settings.json";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
SettingsStore::SettingsStore(std::filesystem::path file) : file_(std::move(file)) {
|
|
85
|
+
if (file_.empty()) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
std::ifstream stream(file_);
|
|
90
|
+
if (!stream) {
|
|
91
|
+
// The normal first run. Not a warning: an application that has never
|
|
92
|
+
// written a setting has no settings file, and saying so at every startup
|
|
93
|
+
// would train people to ignore the log.
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
std::stringstream contents;
|
|
97
|
+
contents << stream.rdbuf();
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
folly::dynamic parsed = folly::parseJson(contents.str());
|
|
101
|
+
if (parsed.isObject()) {
|
|
102
|
+
values_ = std::move(parsed);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
LOG(WARNING) << file_.string() << " holds " << parsed.typeName()
|
|
106
|
+
<< " rather than an object; starting with no settings";
|
|
107
|
+
} catch (const std::exception &error) {
|
|
108
|
+
LOG(WARNING) << "could not read " << file_.string() << ": " << error.what();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
std::vector<std::string> SettingsStore::merge(const folly::dynamic &values) {
|
|
113
|
+
std::vector<std::string> changed;
|
|
114
|
+
if (!values.isObject()) {
|
|
115
|
+
return changed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const auto &pair : values.items()) {
|
|
119
|
+
if (!pair.first.isString()) {
|
|
120
|
+
// JavaScript object keys are strings, so this is unreachable from the
|
|
121
|
+
// module and reachable from a caller in C++.
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const std::string key = pair.first.asString();
|
|
125
|
+
|
|
126
|
+
if (pair.second.isNull()) {
|
|
127
|
+
if (values_.count(key) > 0) {
|
|
128
|
+
values_.erase(key);
|
|
129
|
+
changed.push_back(key);
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (values_.count(key) > 0 && values_[key] == pair.second) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
values_[key] = pair.second;
|
|
138
|
+
changed.push_back(key);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!changed.empty()) {
|
|
142
|
+
save();
|
|
143
|
+
}
|
|
144
|
+
return changed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
std::vector<std::string> SettingsStore::erase(const std::vector<std::string> &keys) {
|
|
148
|
+
std::vector<std::string> removed;
|
|
149
|
+
for (const std::string &key : keys) {
|
|
150
|
+
if (values_.count(key) == 0) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
values_.erase(key);
|
|
154
|
+
removed.push_back(key);
|
|
155
|
+
}
|
|
156
|
+
if (!removed.empty()) {
|
|
157
|
+
save();
|
|
158
|
+
}
|
|
159
|
+
return removed;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
void SettingsStore::save() const {
|
|
163
|
+
if (file_.empty()) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
std::error_code error;
|
|
168
|
+
if (file_.has_parent_path()) {
|
|
169
|
+
std::filesystem::create_directories(file_.parent_path(), error);
|
|
170
|
+
if (error) {
|
|
171
|
+
LOG(WARNING) << "could not create " << file_.parent_path().string() << ": "
|
|
172
|
+
<< error.message();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// A sibling rather than the temporary directory, because the rename below has
|
|
178
|
+
// to stay on one filesystem to be atomic.
|
|
179
|
+
const std::filesystem::path pending(file_.string() + ".new");
|
|
180
|
+
{
|
|
181
|
+
std::ofstream stream(pending, std::ios::binary | std::ios::trunc);
|
|
182
|
+
stream << folly::toPrettyJson(values_) << "\n";
|
|
183
|
+
if (!stream) {
|
|
184
|
+
LOG(WARNING) << "could not write " << pending.string();
|
|
185
|
+
std::error_code ignored;
|
|
186
|
+
std::filesystem::remove(pending, ignored);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
std::filesystem::rename(pending, file_, error);
|
|
192
|
+
if (error) {
|
|
193
|
+
LOG(WARNING) << "could not replace " << file_.string() << ": " << error.message();
|
|
194
|
+
std::error_code ignored;
|
|
195
|
+
std::filesystem::remove(pending, ignored);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
} // namespace basalt
|