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
@@ -0,0 +1,96 @@
1
+ // `textShadowColor`, `textShadowOffset` and `textShadowRadius`, resolved.
2
+ //
3
+ // Three props that every pre-CSS React Native title sets and that no host read.
4
+ // They are `TextAttributes`, so they arrive per *fragment* -- per `<Text>` inside
5
+ // a `<Text>` -- and neither text engine here can draw a different shadow per run:
6
+ // GTK appends one `PangoLayout` to the snapshot, and AppKit draws lines with
7
+ // `CTLineDraw`, which does not honour an `NSShadow` attribute the way AppKit's
8
+ // own text drawing does. So one shadow is taken for the paragraph.
9
+ //
10
+ // ## Which fragment wins, and why that is written down
11
+ //
12
+ // The first fragment that asks for a shadow. That is the same rule
13
+ // `borderStyle` follows for four sides that can only have one stroke, and it is
14
+ // a limit rather than a reading of the spec: `<Text>` with a shadow holding a
15
+ // `<Text>` with a different one draws the outer one twice. Recorded in
16
+ // backlog/text.md rather than approximated, because the alternative -- drawing
17
+ // each run separately -- costs the single-layout arrangement both hosts are
18
+ // built on.
19
+ //
20
+ // ## The radius is a standard deviation, as iOS means it
21
+ //
22
+ // React Native's iOS half puts `textShadowRadius` into `NSShadow.shadowBlurRadius`,
23
+ // which is the `CALayer.shadowRadius` kind of parameter: a gaussian standard
24
+ // deviation rather than CSS's blur radius, which is twice one. So AppKit passes
25
+ // it to `CGContextSetShadowWithColor` unchanged and GTK doubles it for
26
+ // `GskShadow`, which takes CSS's. That is the same conversion, in the same
27
+ // direction, as `filter: drop-shadow(...)`; see core/Filters.h.
28
+
29
+ #pragma once
30
+
31
+ #include <react/renderer/attributedstring/AttributedString.h>
32
+
33
+ #include <cmath>
34
+ #include <optional>
35
+
36
+ namespace basalt {
37
+
38
+ // One text shadow, in the terms both hosts take: an offset, the gaussian's
39
+ // standard deviation, and a straight sRGB colour.
40
+ struct TextShadow {
41
+ float dx{0.0F};
42
+ float dy{0.0F};
43
+ float standardDeviation{0.0F};
44
+ float red{0.0F};
45
+ float green{0.0F};
46
+ float blue{0.0F};
47
+ float alpha{0.0F};
48
+ };
49
+
50
+ // The shadow a paragraph draws, or nothing.
51
+ //
52
+ // A shadow needs a colour with some alpha in it: React Native's default is no
53
+ // colour at all, and a transparent one is a shadow an app asked to be invisible.
54
+ // An offset of nothing and a radius of nothing is also nothing -- a shadow
55
+ // exactly behind the glyphs with no blur cannot be seen -- which is worth
56
+ // skipping rather than drawing, because both hosts pay for a shadow pass.
57
+ inline std::optional<TextShadow> textShadow(
58
+ const facebook::react::AttributedString &attributedString) {
59
+ for (const auto &fragment : attributedString.getFragments()) {
60
+ const auto &attributes = fragment.textAttributes;
61
+ if (!attributes.textShadowColor) {
62
+ continue;
63
+ }
64
+ const auto components =
65
+ facebook::react::colorComponentsFromColor(attributes.textShadowColor);
66
+ if (components.alpha <= 0.0F) {
67
+ continue;
68
+ }
69
+
70
+ TextShadow shadow;
71
+ if (attributes.textShadowOffset.has_value()) {
72
+ shadow.dx = static_cast<float>(attributes.textShadowOffset->width);
73
+ shadow.dy = static_cast<float>(attributes.textShadowOffset->height);
74
+ }
75
+ // NaN is how React Native spells "not set" for a Float, so a radius that is
76
+ // not a number is zero rather than a crash in a blur.
77
+ if (!std::isnan(attributes.textShadowRadius)) {
78
+ shadow.standardDeviation = static_cast<float>(attributes.textShadowRadius);
79
+ }
80
+ if (shadow.standardDeviation < 0.0F) {
81
+ shadow.standardDeviation = 0.0F;
82
+ }
83
+ if (shadow.dx == 0.0F && shadow.dy == 0.0F && shadow.standardDeviation == 0.0F) {
84
+ continue;
85
+ }
86
+
87
+ shadow.red = components.red;
88
+ shadow.green = components.green;
89
+ shadow.blue = components.blue;
90
+ shadow.alpha = components.alpha;
91
+ return shadow;
92
+ }
93
+ return std::nullopt;
94
+ }
95
+
96
+ } // namespace basalt
@@ -0,0 +1,33 @@
1
+ // Where a paragraph sits in a box taller than it is: the arithmetic half.
2
+ //
3
+ // Separate from `core/TextAlignments.h` for one reason, and it is the reason
4
+ // `FocusRing.h` and `ControlMetrics.h` are separate too: this is needed by each
5
+ // host's *widget* layer -- `RnView.cpp`, `RnTextLayout.mm`,
6
+ // `RnWin32TextLayout.cpp` -- and that layer has no React Native in it and
7
+ // cannot include a header that names `TextAlignmentVertical`. Each of those
8
+ // three draws the paragraph, and the box it is drawing into is only known then:
9
+ // a view resized without its props changing has new slack and the same text.
10
+ //
11
+ // So the enum lives in TextAlignments.h with the rest of the alignment
12
+ // decisions, and the one line all three draw with lives here.
13
+
14
+ #pragma once
15
+
16
+ namespace basalt {
17
+
18
+ // How far down to start drawing, given the box and what the text needs.
19
+ //
20
+ // Never negative: a paragraph taller than its box starts at the top, however it
21
+ // was aligned, because the alternative is text pushed out through the top edge
22
+ // where there is nothing to scroll it back. iOS and Android both do this, and a
23
+ // `numberOfLines` paragraph is the case -- the box is the lines that fit and the
24
+ // text is all of them.
25
+ inline double textVerticalOffset(double boxHeight, double textHeight, double flush) {
26
+ const double slack = boxHeight - textHeight;
27
+ if (slack <= 0.0 || flush <= 0.0) {
28
+ return 0.0;
29
+ }
30
+ return slack * flush;
31
+ }
32
+
33
+ } // namespace basalt
@@ -1,5 +1,9 @@
1
1
  #include "UIManagerAccess.h"
2
2
 
3
+ #include <glog/logging.h>
4
+
5
+ #include <mutex>
6
+
3
7
  namespace basalt {
4
8
 
5
9
  namespace {
@@ -19,6 +23,38 @@ std::shared_ptr<facebook::react::UIManager> sharedUIManager() {
19
23
  return held().lock();
20
24
  }
21
25
 
26
+ bool reportMountedSurface(facebook::react::SurfaceId surfaceId) {
27
+ const auto uiManager = sharedUIManager();
28
+ if (!uiManager) {
29
+ return false;
30
+ }
31
+ // `visit` answers false without running the callback when the surface is not
32
+ // there, and says it is safe from any thread. See the header for why this
33
+ // question is the one that matters.
34
+ const bool registered = uiManager->getShadowTreeRegistry().visit(
35
+ surfaceId, [](const facebook::react::ShadowTree &) {});
36
+ if (!registered) {
37
+ // Every time, because this should be rare: it means a transaction was
38
+ // applied after its surface had gone, which is the race this guards. A run
39
+ // that logs this in quantity is saying the refusal is not rare, and a
40
+ // *healthy* run that logs it at all is saying the question is wrong.
41
+ LOG(WARNING) << "mount report refused: surface " << surfaceId
42
+ << " is no longer registered";
43
+ return false;
44
+ }
45
+ // Once per process, so that a suite can tell "the guard is letting mounts
46
+ // through" from "the guard refuses everything". Refusing everything is the
47
+ // failure this has no other way of noticing: nothing asserts that a mount
48
+ // hook ran, so an over-eager refusal would stop Reanimated silently and leave
49
+ // every test green. See docs/backlog/testing.md.
50
+ static std::once_flag reported;
51
+ std::call_once(reported, [surfaceId] {
52
+ LOG(INFO) << "mount reported to the UIManager, surface " << surfaceId;
53
+ });
54
+ uiManager->reportMount(surfaceId);
55
+ return true;
56
+ }
57
+
22
58
  namespace {
23
59
 
24
60
  EventListenerInstaller &installer() {
@@ -24,6 +24,34 @@ namespace basalt {
24
24
  void setSharedUIManager(std::weak_ptr<facebook::react::UIManager> uiManager);
25
25
  std::shared_ptr<facebook::react::UIManager> sharedUIManager();
26
26
 
27
+ // `reportMount`, but only for a surface the UIManager still has, which is a
28
+ // workaround for an upstream use-after-free and not a tidying-up.
29
+ //
30
+ // `UIManager::mountHooks_` holds raw, non-owning pointers and unregistering is
31
+ // the owner's job. `Scheduler` registers one and `~Scheduler` does not take it
32
+ // out again, so from the moment `destroyReactInstance` writes
33
+ // `scheduler_ = nullptr` the list contains a freed `EventPerformanceLogger` --
34
+ // three statements before it tells a mounting manager anything, so the epoch
35
+ // guard in MountingWalk.h cannot help. A mount draining on the main queue in
36
+ // that gap used to walk the list and call a virtual method on freed memory.
37
+ // See docs/backlog/testing.md.
38
+ //
39
+ // What makes a probe enough: `stopAllSurfaces()` runs *before* the Scheduler is
40
+ // freed and takes the shadow trees out of the registry, so "the hook is
41
+ // dangling" always implies "the surface has gone". Asking whether the surface is
42
+ // still registered therefore refuses exactly the dangerous case, and the healthy
43
+ // case is unaffected because a live surface is still in there. It also skips a
44
+ // call that had nothing to report: with no root shadow node `reportMount` only
45
+ // tells every hook the tree unmounted.
46
+ //
47
+ // Not airtight, and it is worth being clear about that. The surface could in
48
+ // principle be stopped between the question and the call, which is a few
49
+ // instructions rather than three statements of teardown. The fix is one line
50
+ // upstream, in `~Scheduler`.
51
+ //
52
+ // Returns whether it reported.
53
+ bool reportMountedSurface(facebook::react::SurfaceId surfaceId);
54
+
27
55
  // Watching every event Fabric dispatches.
28
56
  //
29
57
  // `Scheduler::addEventListener` is how a library sees events before the
@@ -166,22 +166,79 @@ WindowBounds lastKnownWindowBounds() {
166
166
  return cache();
167
167
  }
168
168
 
169
+ namespace {
170
+
171
+ std::function<void()> &dimensionsListener() {
172
+ static std::function<void()> value;
173
+ return value;
174
+ }
175
+
176
+ } // namespace
177
+
178
+ namespace {
179
+
180
+ // The main surface's size, which is the window minus whatever the desktop drew
181
+ // around it. See the header.
182
+ std::pair<double, double> &surfaceSize() {
183
+ static std::pair<double, double> value{0.0, 0.0};
184
+ return value;
185
+ }
186
+
187
+ } // namespace
188
+
189
+ void notifySurfaceSize(double width, double height) {
190
+ std::function<void()> toCall;
191
+ {
192
+ const std::lock_guard<std::mutex> guard(lock());
193
+ if (surfaceSize().first == width && surfaceSize().second == height) {
194
+ return;
195
+ }
196
+ surfaceSize() = {width, height};
197
+ toCall = dimensionsListener();
198
+ }
199
+ // Outside the lock, which emits into the runtime; see
200
+ // notifyWindowBoundsChanged below.
201
+ if (toCall) {
202
+ toCall();
203
+ }
204
+ }
205
+
206
+ void lastKnownSurfaceSize(double *outWidth, double *outHeight) {
207
+ const std::lock_guard<std::mutex> guard(lock());
208
+ if (outWidth != nullptr) {
209
+ *outWidth = surfaceSize().first;
210
+ }
211
+ if (outHeight != nullptr) {
212
+ *outHeight = surfaceSize().second;
213
+ }
214
+ }
215
+
216
+ void setDimensionsListener(std::function<void()> listener) {
217
+ const std::lock_guard<std::mutex> guard(lock());
218
+ dimensionsListener() = std::move(listener);
219
+ }
220
+
169
221
  void notifyWindowBoundsChanged() {
170
222
  // Asked on the UI thread, which is where this is called from, and stored
171
223
  // where the JavaScript thread can read it.
172
224
  const WindowBounds bounds = windowBounds();
173
225
 
174
226
  std::function<void(const WindowBounds &)> toCall;
227
+ std::function<void()> toCallForDimensions;
175
228
  {
176
229
  const std::lock_guard<std::mutex> guard(lock());
177
230
  cache() = bounds;
178
231
  toCall = listener();
232
+ toCallForDimensions = dimensionsListener();
179
233
  }
180
- // Called outside the lock: it emits a device event, and holding a lock across
234
+ // Called outside the lock: they emit device events, and holding a lock across
181
235
  // a call into the runtime is how a deadlock is built.
182
236
  if (toCall) {
183
237
  toCall(bounds);
184
238
  }
239
+ if (toCallForDimensions) {
240
+ toCallForDimensions();
241
+ }
185
242
  }
186
243
 
187
244
  // --- Windows closing themselves ----------------------------------------------
@@ -70,6 +70,30 @@ void setWindowFullScreen(bool fullScreen);
70
70
  // nothing removes it.
71
71
  void setWindowBoundsListener(std::function<void(const WindowBounds &)> listener);
72
72
 
73
+ // How big the area React lays out in is, which is not the window's frame: a
74
+ // title bar belongs to the window and not to the app.
75
+ //
76
+ // Reported by each host wherever it starts or re-constrains its *main* surface,
77
+ // which is the one place that already knows this number -- and the number
78
+ // `Dimensions.get('window')` should answer with, since it is what an app can
79
+ // put something in. A second window and the error inspector have surfaces of
80
+ // their own and are deliberately not reported: `Dimensions` is singular, and
81
+ // the app's window is the one it means.
82
+ void notifySurfaceSize(double width, double height);
83
+
84
+ // The last size reported, or zeroes before the first surface starts. Readable
85
+ // from any thread, like the bounds above and for the same reason.
86
+ void lastKnownSurfaceSize(double *outWidth, double *outHeight);
87
+
88
+ // A second slot on the same signal, for `Dimensions`.
89
+ //
90
+ // Two rather than one because the two listeners have different lifetimes and
91
+ // neither owns the other: `useWindow()`'s module exists only in an app that
92
+ // imports this package, and React Native's `DeviceInfo` module exists in every
93
+ // app -- `Dimensions.js` requires it at import. A single slot would mean
94
+ // whichever was constructed second silently took the other's events.
95
+ void setDimensionsListener(std::function<void()> listener);
96
+
73
97
  // The host's half of that: called from wherever it already learns the window
74
98
  // moved or resized, on the UI thread. Also updates the cache below, so it is
75
99
  // worth calling once when the window is first shown even if nothing is
@@ -0,0 +1,52 @@
1
+ // `writingDirection`, as a name both hosts print.
2
+ //
3
+ // The prop decides which edge a paragraph starts from, and the two text engines
4
+ // take it in their own terms: a Pango context's base direction, an
5
+ // `NSParagraphStyle`'s `baseWritingDirection`. What is shared is only the word
6
+ // the tree dump reports, which exists so the two dumps compare line by line.
7
+ //
8
+ // `natural` is printed rather than left out. Asking for the Unicode algorithm's
9
+ // answer is not the same as saying nothing: a nested `<Text>` inherits the
10
+ // enclosing paragraph's direction when nothing is said, and the algorithm's
11
+ // answer when `natural` is.
12
+
13
+ #pragma once
14
+
15
+ #include <react/renderer/attributedstring/AttributedString.h>
16
+
17
+ #include <optional>
18
+
19
+ namespace basalt {
20
+
21
+ // The direction the paragraph asked for, from the first fragment that has one.
22
+ // React Native resolves a paragraph's attributes onto every fragment in it, so
23
+ // the first is the paragraph's; a nested `<Text>` with its own direction is a
24
+ // limit this shares with the text shadow, and backlog/text.md records it.
25
+ inline std::optional<facebook::react::WritingDirection> writingDirection(
26
+ const facebook::react::AttributedString &attributedString) {
27
+ for (const auto &fragment : attributedString.getFragments()) {
28
+ if (fragment.textAttributes.baseWritingDirection.has_value()) {
29
+ return fragment.textAttributes.baseWritingDirection;
30
+ }
31
+ }
32
+ return std::nullopt;
33
+ }
34
+
35
+ // Its name, or nullptr when the app asked for nothing.
36
+ inline const char *writingDirectionName(
37
+ std::optional<facebook::react::WritingDirection> direction) {
38
+ if (!direction.has_value()) {
39
+ return nullptr;
40
+ }
41
+ switch (*direction) {
42
+ case facebook::react::WritingDirection::Natural:
43
+ return "natural";
44
+ case facebook::react::WritingDirection::LeftToRight:
45
+ return "ltr";
46
+ case facebook::react::WritingDirection::RightToLeft:
47
+ return "rtl";
48
+ }
49
+ return nullptr;
50
+ }
51
+
52
+ } // namespace basalt
@@ -109,6 +109,13 @@ void showMenu(const MenuRequest &, MenuCallback onChosen) {
109
109
  onChosen(-1);
110
110
  }
111
111
  }
112
+ // And its roles. A platform that cannot show a menu performs nothing in one, so
113
+ // every role is unsupported here and `entriesFrom` drops them all -- which is
114
+ // the same answer this file gives everywhere: no, and in the right shape.
115
+ bool menuRoleSupported(const std::string &) {
116
+ return false;
117
+ }
118
+ void performMenuRole(const std::string &) {}
112
119
  // Added when the gesture recognisers arrived: "run this on the UI thread", now
113
120
  // and later. A platform with a run loop has both already; this one has neither
114
121
  // and says so.
@@ -0,0 +1,161 @@
1
+ // A font file the machine does not have, built from one it does.
2
+ //
3
+ // Shared by both suites' font tests, because both have the same problem. The
4
+ // case `expo-font` exists for is a family the system has never heard of: an app
5
+ // ships Inter, loads it at runtime, and `fontFamily: 'Inter'` has to start
6
+ // working. A test that registers a font already in the system index proves the
7
+ // name mapping and nothing else, because every layer underneath already knew
8
+ // that family -- which is exactly how GTK's Pango-context reload and macOS's
9
+ // real registration path came to have no test.
10
+ //
11
+ // So this takes a real font file and renames the family *inside* it, which is
12
+ // the one thing that makes it new. The rename is done in place, one byte per
13
+ // occurrence, so nothing in the file moves: a name record's length and every
14
+ // offset after it stay valid, and no checksum has to be recomputed. The
15
+ // replacement letter is applied to the family's first character wherever the
16
+ // name table spells it, in ASCII and in the UTF-16BE that Windows-platform
17
+ // name records use.
18
+ //
19
+ // Shipping a fixture font would be the other way to do this, and it would mean
20
+ // a binary in the repository plus its licence, for a file that would still have
21
+ // to be renamed to be sure the machine lacks it.
22
+
23
+ #pragma once
24
+
25
+ #include <cstdint>
26
+ #include <cstdio>
27
+ #include <string>
28
+ #include <vector>
29
+
30
+ namespace basalt::test {
31
+
32
+ struct RenamedFont {
33
+ // Empty when the source could not be read or has no name table to patch, so
34
+ // a test can say so rather than assert on a file that was never written.
35
+ std::string path;
36
+ std::string family;
37
+ };
38
+
39
+ namespace detail {
40
+
41
+ inline std::uint16_t beU16(const unsigned char *at) {
42
+ return static_cast<std::uint16_t>((at[0] << 8) | at[1]);
43
+ }
44
+
45
+ inline std::uint32_t beU32(const unsigned char *at) {
46
+ return (static_cast<std::uint32_t>(at[0]) << 24) | (static_cast<std::uint32_t>(at[1]) << 16) |
47
+ (static_cast<std::uint32_t>(at[2]) << 8) | static_cast<std::uint32_t>(at[3]);
48
+ }
49
+
50
+ // Replaces one byte in every occurrence of `needle` between `from` and `to`.
51
+ // `at` picks which byte of the match, which is 0 for ASCII and 1 for UTF-16BE,
52
+ // where the high byte of a Latin letter is zero.
53
+ inline int patchOccurrences(std::vector<unsigned char> &bytes,
54
+ std::size_t from,
55
+ std::size_t to,
56
+ const std::vector<unsigned char> &needle,
57
+ std::size_t at,
58
+ unsigned char replacement) {
59
+ if (needle.empty() || to > bytes.size() || from >= to) {
60
+ return 0;
61
+ }
62
+ int patched = 0;
63
+ for (std::size_t i = from; i + needle.size() <= to; i++) {
64
+ bool same = true;
65
+ for (std::size_t j = 0; j < needle.size(); j++) {
66
+ if (bytes[i + j] != needle[j]) {
67
+ same = false;
68
+ break;
69
+ }
70
+ }
71
+ if (same) {
72
+ bytes[i + at] = replacement;
73
+ patched++;
74
+ i += needle.size() - 1;
75
+ }
76
+ }
77
+ return patched;
78
+ }
79
+
80
+ } // namespace detail
81
+
82
+ // Writes `source` to `destination` with `family`'s first letter replaced by
83
+ // `replacement` wherever the name table spells it, and answers with the new
84
+ // family name. `family` is the name the file calls itself, which is what the
85
+ // platform's own registry reads back.
86
+ inline RenamedFont renameFontFile(const std::string &source,
87
+ const std::string &family,
88
+ const std::string &destination,
89
+ char replacement = 'Q') {
90
+ if (family.empty() || family[0] == replacement) {
91
+ return {};
92
+ }
93
+
94
+ std::FILE *in = std::fopen(source.c_str(), "rb");
95
+ if (in == nullptr) {
96
+ return {};
97
+ }
98
+ std::vector<unsigned char> bytes;
99
+ unsigned char chunk[16384];
100
+ while (const std::size_t read = std::fread(chunk, 1, sizeof(chunk), in)) {
101
+ bytes.insert(bytes.end(), chunk, chunk + read);
102
+ }
103
+ std::fclose(in);
104
+
105
+ // A collection holds several fonts and starts with its own header; the table
106
+ // directory is not where this expects it, so say no rather than patch bytes
107
+ // at a guess.
108
+ if (bytes.size() < 12 || detail::beU32(bytes.data()) == 0x74746366 /* 'ttcf' */) {
109
+ return {};
110
+ }
111
+
112
+ const std::uint16_t tables = detail::beU16(bytes.data() + 4);
113
+ std::size_t nameAt = 0;
114
+ std::size_t nameLength = 0;
115
+ for (std::uint16_t i = 0; i < tables; i++) {
116
+ const std::size_t record = 12 + std::size_t{16} * i;
117
+ if (record + 16 > bytes.size()) {
118
+ return {};
119
+ }
120
+ if (detail::beU32(bytes.data() + record) == 0x6E616D65 /* 'name' */) {
121
+ nameAt = detail::beU32(bytes.data() + record + 8);
122
+ nameLength = detail::beU32(bytes.data() + record + 12);
123
+ break;
124
+ }
125
+ }
126
+ if (nameAt == 0 || nameLength == 0 || nameAt + nameLength > bytes.size()) {
127
+ return {};
128
+ }
129
+
130
+ const std::vector<unsigned char> ascii(family.begin(), family.end());
131
+ std::vector<unsigned char> utf16;
132
+ utf16.reserve(family.size() * 2);
133
+ for (const char character : family) {
134
+ utf16.push_back(0);
135
+ utf16.push_back(static_cast<unsigned char>(character));
136
+ }
137
+
138
+ int patched = detail::patchOccurrences(bytes, nameAt, nameAt + nameLength, ascii, 0,
139
+ static_cast<unsigned char>(replacement));
140
+ patched += detail::patchOccurrences(bytes, nameAt, nameAt + nameLength, utf16, 1,
141
+ static_cast<unsigned char>(replacement));
142
+ if (patched == 0) {
143
+ return {};
144
+ }
145
+
146
+ std::FILE *out = std::fopen(destination.c_str(), "wb");
147
+ if (out == nullptr) {
148
+ return {};
149
+ }
150
+ const std::size_t written = std::fwrite(bytes.data(), 1, bytes.size(), out);
151
+ std::fclose(out);
152
+ if (written != bytes.size()) {
153
+ return {};
154
+ }
155
+
156
+ std::string renamed = family;
157
+ renamed[0] = replacement;
158
+ return RenamedFont{destination, renamed};
159
+ }
160
+
161
+ } // namespace basalt::test
@@ -6,8 +6,11 @@
6
6
 
7
7
  #include "TestHarness.h"
8
8
 
9
+ #include "CrashHandler.h"
10
+
9
11
  #include <algorithm>
10
12
  #include <cctype>
13
+ #include <iostream>
11
14
  #include <sstream>
12
15
 
13
16
  namespace basalt::testing {
@@ -47,6 +50,21 @@ std::string lowered(std::string text) {
47
50
  } // namespace
48
51
 
49
52
  int runAllTests(int argc, char **argv) {
53
+ // Unbuffered, and this is not a style choice. `basalt_appkit_tests`
54
+ // segfaulted on CI's Mac and the log said only "Segmentation fault: 11": not
55
+ // one test name, because stdout to a pipe is block-buffered and the buffer
56
+ // dies with the process. On a developer's machine it is a terminal and
57
+ // line-buffered, so the same crash would have named the test.
58
+ //
59
+ // A unit suite that cannot say where it died is a suite that costs a CI round
60
+ // trip per guess, which is what this cost.
61
+ std::cout << std::unitbuf;
62
+
63
+ // And the handler from core/CrashHandler.h, for the half a flush cannot give:
64
+ // which frame faulted. The hosts install it at startup and the suites did
65
+ // not, so a crash in a *test* said less than a crash in an app.
66
+ basalt::installCrashHandler();
67
+
50
68
  // Arguments are substrings of test names, and a test runs if it matches any
51
69
  // of them. There is a lot of the day in the difference between running one
52
70
  // test and running all of them, and this runner used to accept an argument