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.
Files changed (143) 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/packageApp.d.ts.map +1 -1
  6. package/dist/cli/packageApp.js +7 -1
  7. package/dist/cli/packageApp.js.map +1 -1
  8. package/dist/metro-config.d.ts.map +1 -1
  9. package/dist/metro-config.js +53 -5
  10. package/dist/metro-config.js.map +1 -1
  11. package/dist/src/KeyHandler.d.ts +1 -1
  12. package/dist/src/KeyHandler.d.ts.map +1 -1
  13. package/dist/src/KeyHandler.js.map +1 -1
  14. package/dist/src/index.d.ts +5 -0
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/overrides/Alert.d.ts +1 -1
  17. package/dist/src/overrides/Alert.js +1 -1
  18. package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
  19. package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
  20. package/dist/src/overrides/BaseViewConfig.js +21 -3
  21. package/dist/src/overrides/BaseViewConfig.js.map +1 -1
  22. package/dist/src/overrides/Button.d.ts +81 -0
  23. package/dist/src/overrides/Button.d.ts.map +1 -0
  24. package/dist/src/overrides/Button.js +109 -0
  25. package/dist/src/overrides/Button.js.map +1 -0
  26. package/dist/src/overrides/ScrollView.d.ts +25 -2
  27. package/dist/src/overrides/ScrollView.d.ts.map +1 -1
  28. package/dist/src/overrides/ScrollView.js +64 -7
  29. package/dist/src/overrides/ScrollView.js.map +1 -1
  30. package/dist/src/overrides/Settings.d.ts +81 -0
  31. package/dist/src/overrides/Settings.d.ts.map +1 -0
  32. package/dist/src/overrides/Settings.js +120 -0
  33. package/dist/src/overrides/Settings.js.map +1 -0
  34. package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
  35. package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
  36. package/dist/src/overrides/processDecelerationRate.js +31 -0
  37. package/dist/src/overrides/processDecelerationRate.js.map +1 -0
  38. package/dist/src/useContextMenu.d.ts +57 -6
  39. package/dist/src/useContextMenu.d.ts.map +1 -1
  40. package/dist/src/useContextMenu.js +34 -1
  41. package/dist/src/useContextMenu.js.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/native/CMakeLists.txt +51 -0
  44. package/native/core/ActionSheet.cpp +75 -0
  45. package/native/core/ActionSheet.h +90 -0
  46. package/native/core/AppStateHost.cpp +76 -0
  47. package/native/core/AppStateHost.h +68 -0
  48. package/native/core/BackgroundLayers.h +282 -0
  49. package/native/core/BlendModes.h +67 -0
  50. package/native/core/CoreModules.cpp +380 -11
  51. package/native/core/CoreModules.h +140 -4
  52. package/native/core/CrashHandler.cpp +246 -0
  53. package/native/core/CrashHandler.h +65 -0
  54. package/native/core/CursorNames.h +110 -0
  55. package/native/core/DevMenu.cpp +6 -5
  56. package/native/core/DeviceMetrics.cpp +87 -0
  57. package/native/core/DeviceMetrics.h +80 -0
  58. package/native/core/Filters.h +336 -0
  59. package/native/core/FontFitting.h +183 -0
  60. package/native/core/FontScaling.h +153 -0
  61. package/native/core/FontVariants.h +148 -0
  62. package/native/core/Gradients.h +523 -0
  63. package/native/core/ImageAnimation.h +109 -0
  64. package/native/core/ImageBytes.cpp +28 -1
  65. package/native/core/ImageBytes.h +17 -1
  66. package/native/core/ImageProgress.h +85 -0
  67. package/native/core/LabelRegistry.h +189 -0
  68. package/native/core/LegacyShadow.h +90 -0
  69. package/native/core/LiveRegions.h +114 -0
  70. package/native/core/MenuModule.cpp +35 -4
  71. package/native/core/MountingWalk.h +125 -2
  72. package/native/core/PlatformServices.h +187 -3
  73. package/native/core/SafeAreaComponent.cpp +115 -0
  74. package/native/core/SafeAreaComponent.h +105 -0
  75. package/native/core/ScreensComponent.cpp +201 -0
  76. package/native/core/ScreensComponent.h +216 -0
  77. package/native/core/ScrollBounds.cpp +8 -0
  78. package/native/core/ScrollBounds.h +14 -0
  79. package/native/core/ScrollIndicator.h +67 -0
  80. package/native/core/ScrollMomentum.cpp +24 -0
  81. package/native/core/ScrollMomentum.h +14 -0
  82. package/native/core/ScrollSnap.cpp +121 -12
  83. package/native/core/ScrollSnap.h +45 -0
  84. package/native/core/ScrollVisiblePosition.cpp +68 -0
  85. package/native/core/ScrollVisiblePosition.h +101 -0
  86. package/native/core/SettingsStore.cpp +199 -0
  87. package/native/core/SettingsStore.h +127 -0
  88. package/native/core/ShadowShape.h +38 -0
  89. package/native/core/SystemLocale.cpp +103 -0
  90. package/native/core/SystemLocale.h +86 -0
  91. package/native/core/TestDialog.cpp +45 -6
  92. package/native/core/TestDialog.h +15 -0
  93. package/native/core/TextAlignments.h +240 -0
  94. package/native/core/TextChecking.h +133 -0
  95. package/native/core/TextColors.h +83 -0
  96. package/native/core/TextDecorations.h +101 -0
  97. package/native/core/TextDirection.cpp +242 -0
  98. package/native/core/TextDirection.h +88 -0
  99. package/native/core/TextHighlight.h +30 -0
  100. package/native/core/TextSelection.cpp +97 -0
  101. package/native/core/TextSelection.h +161 -0
  102. package/native/core/TextShadows.h +96 -0
  103. package/native/core/TextVerticalAlign.h +33 -0
  104. package/native/core/UIManagerAccess.cpp +36 -0
  105. package/native/core/UIManagerAccess.h +28 -0
  106. package/native/core/WindowBoundsCache.cpp +58 -1
  107. package/native/core/WindowControl.h +24 -0
  108. package/native/core/WritingDirections.h +52 -0
  109. package/native/core/portability_probe.cpp +7 -0
  110. package/native/tests/RenamedFontFile.h +161 -0
  111. package/native/tests/TestHarness.cpp +18 -0
  112. package/native/tests/test_action_sheet.cpp +157 -0
  113. package/native/tests/test_app_state.cpp +60 -0
  114. package/native/tests/test_background_layers.cpp +281 -0
  115. package/native/tests/test_blend_modes.cpp +79 -0
  116. package/native/tests/test_blobs.cpp +1 -1
  117. package/native/tests/test_device_metrics.cpp +126 -0
  118. package/native/tests/test_filters.cpp +271 -0
  119. package/native/tests/test_font_fitting.cpp +172 -0
  120. package/native/tests/test_font_scaling.cpp +127 -0
  121. package/native/tests/test_font_variants.cpp +84 -0
  122. package/native/tests/test_gradients.cpp +390 -0
  123. package/native/tests/test_image_animation.cpp +105 -0
  124. package/native/tests/test_image_progress.cpp +94 -0
  125. package/native/tests/test_label_registry.cpp +262 -0
  126. package/native/tests/test_legacy_shadow.cpp +165 -0
  127. package/native/tests/test_live_regions.cpp +108 -0
  128. package/native/tests/test_menu_entries.cpp +192 -0
  129. package/native/tests/test_momentum.cpp +45 -0
  130. package/native/tests/test_scroll_bounds.cpp +42 -0
  131. package/native/tests/test_scroll_indicator.cpp +61 -0
  132. package/native/tests/test_scroll_snap.cpp +129 -0
  133. package/native/tests/test_scroll_visible_position.cpp +121 -0
  134. package/native/tests/test_settings_store.cpp +257 -0
  135. package/native/tests/test_shadow_shape.cpp +47 -0
  136. package/native/tests/test_system_locale.cpp +36 -0
  137. package/native/tests/test_text_alignment.cpp +299 -0
  138. package/native/tests/test_text_checking.cpp +31 -0
  139. package/native/tests/test_text_colors.cpp +92 -0
  140. package/native/tests/test_text_decorations.cpp +100 -0
  141. package/native/tests/test_text_selection.cpp +168 -0
  142. package/native/tests/test_text_shadows.cpp +132 -0
  143. 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
- std::optional<double> nearestOffset(const std::vector<double> &offsets,
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 velocity) {
33
- if (offsets.empty()) {
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() - offset);
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 - offset);
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 strictly past where we are. A flick forwards must not
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(config.offsets, offset, velocity);
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 = offset + shift;
210
+ const double aligned = landing + shift;
102
211
 
103
212
  double index = std::floor(aligned / interval);
104
213
  const double within = aligned - index * interval;
@@ -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