basalt-core 0.0.0-rc5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/cli/basalt.d.ts.map +1 -1
  2. package/dist/cli/basalt.js +18 -1
  3. package/dist/cli/basalt.js.map +1 -1
  4. package/dist/cli/bundle-cli.js +0 -0
  5. package/dist/cli/desktop.d.ts.map +1 -1
  6. package/dist/cli/desktop.js +53 -4
  7. package/dist/cli/desktop.js.map +1 -1
  8. package/dist/cli/packageApp.d.ts.map +1 -1
  9. package/dist/cli/packageApp.js +7 -1
  10. package/dist/cli/packageApp.js.map +1 -1
  11. package/dist/metro-config.d.ts.map +1 -1
  12. package/dist/metro-config.js +53 -5
  13. package/dist/metro-config.js.map +1 -1
  14. package/dist/src/KeyHandler.d.ts +1 -1
  15. package/dist/src/KeyHandler.d.ts.map +1 -1
  16. package/dist/src/KeyHandler.js.map +1 -1
  17. package/dist/src/index.d.ts +5 -0
  18. package/dist/src/index.d.ts.map +1 -1
  19. package/dist/src/overrides/Alert.d.ts +1 -1
  20. package/dist/src/overrides/Alert.js +1 -1
  21. package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
  22. package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
  23. package/dist/src/overrides/BaseViewConfig.js +21 -3
  24. package/dist/src/overrides/BaseViewConfig.js.map +1 -1
  25. package/dist/src/overrides/Button.d.ts +81 -0
  26. package/dist/src/overrides/Button.d.ts.map +1 -0
  27. package/dist/src/overrides/Button.js +109 -0
  28. package/dist/src/overrides/Button.js.map +1 -0
  29. package/dist/src/overrides/ScrollView.d.ts +25 -2
  30. package/dist/src/overrides/ScrollView.d.ts.map +1 -1
  31. package/dist/src/overrides/ScrollView.js +64 -7
  32. package/dist/src/overrides/ScrollView.js.map +1 -1
  33. package/dist/src/overrides/Settings.d.ts +81 -0
  34. package/dist/src/overrides/Settings.d.ts.map +1 -0
  35. package/dist/src/overrides/Settings.js +120 -0
  36. package/dist/src/overrides/Settings.js.map +1 -0
  37. package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
  38. package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
  39. package/dist/src/overrides/processDecelerationRate.js +31 -0
  40. package/dist/src/overrides/processDecelerationRate.js.map +1 -0
  41. package/dist/src/useContextMenu.d.ts +57 -6
  42. package/dist/src/useContextMenu.d.ts.map +1 -1
  43. package/dist/src/useContextMenu.js +34 -1
  44. package/dist/src/useContextMenu.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/native/CMakeLists.txt +85 -1
  47. package/native/cmake/Skia.cmake +29 -6
  48. package/native/core/ActionSheet.cpp +75 -0
  49. package/native/core/ActionSheet.h +90 -0
  50. package/native/core/AppStateHost.cpp +76 -0
  51. package/native/core/AppStateHost.h +68 -0
  52. package/native/core/BackgroundLayers.h +282 -0
  53. package/native/core/BlendModes.h +67 -0
  54. package/native/core/CoreModules.cpp +380 -11
  55. package/native/core/CoreModules.h +140 -4
  56. package/native/core/CrashHandler.cpp +246 -0
  57. package/native/core/CrashHandler.h +65 -0
  58. package/native/core/CursorNames.h +110 -0
  59. package/native/core/DevMenu.cpp +6 -5
  60. package/native/core/DeviceMetrics.cpp +87 -0
  61. package/native/core/DeviceMetrics.h +80 -0
  62. package/native/core/Filters.h +336 -0
  63. package/native/core/FontFitting.h +183 -0
  64. package/native/core/FontScaling.h +153 -0
  65. package/native/core/FontVariants.h +148 -0
  66. package/native/core/Gradients.h +523 -0
  67. package/native/core/ImageAnimation.h +109 -0
  68. package/native/core/ImageBytes.cpp +28 -1
  69. package/native/core/ImageBytes.h +17 -1
  70. package/native/core/ImageProgress.h +85 -0
  71. package/native/core/LabelRegistry.h +189 -0
  72. package/native/core/LegacyShadow.h +90 -0
  73. package/native/core/LiveRegions.h +114 -0
  74. package/native/core/MenuModule.cpp +35 -4
  75. package/native/core/MountingWalk.h +125 -2
  76. package/native/core/PlatformServices.h +187 -3
  77. package/native/core/SafeAreaComponent.cpp +115 -0
  78. package/native/core/SafeAreaComponent.h +105 -0
  79. package/native/core/ScreensComponent.cpp +201 -0
  80. package/native/core/ScreensComponent.h +216 -0
  81. package/native/core/ScrollBounds.cpp +8 -0
  82. package/native/core/ScrollBounds.h +14 -0
  83. package/native/core/ScrollIndicator.h +67 -0
  84. package/native/core/ScrollMomentum.cpp +24 -0
  85. package/native/core/ScrollMomentum.h +14 -0
  86. package/native/core/ScrollSnap.cpp +121 -12
  87. package/native/core/ScrollSnap.h +45 -0
  88. package/native/core/ScrollVisiblePosition.cpp +68 -0
  89. package/native/core/ScrollVisiblePosition.h +101 -0
  90. package/native/core/SettingsStore.cpp +199 -0
  91. package/native/core/SettingsStore.h +127 -0
  92. package/native/core/ShadowShape.h +38 -0
  93. package/native/core/SkiaViewComponent.cpp +11 -0
  94. package/native/core/SkiaViewComponent.h +50 -0
  95. package/native/core/SystemLocale.cpp +103 -0
  96. package/native/core/SystemLocale.h +86 -0
  97. package/native/core/TestDialog.cpp +45 -6
  98. package/native/core/TestDialog.h +15 -0
  99. package/native/core/TextAlignments.h +240 -0
  100. package/native/core/TextChecking.h +133 -0
  101. package/native/core/TextColors.h +83 -0
  102. package/native/core/TextDecorations.h +101 -0
  103. package/native/core/TextDirection.cpp +242 -0
  104. package/native/core/TextDirection.h +88 -0
  105. package/native/core/TextHighlight.h +30 -0
  106. package/native/core/TextSelection.cpp +97 -0
  107. package/native/core/TextSelection.h +161 -0
  108. package/native/core/TextShadows.h +96 -0
  109. package/native/core/TextVerticalAlign.h +33 -0
  110. package/native/core/UIManagerAccess.cpp +36 -0
  111. package/native/core/UIManagerAccess.h +28 -0
  112. package/native/core/WindowBoundsCache.cpp +58 -1
  113. package/native/core/WindowControl.h +24 -0
  114. package/native/core/WritingDirections.h +52 -0
  115. package/native/core/portability_probe.cpp +7 -0
  116. package/native/tests/RenamedFontFile.h +161 -0
  117. package/native/tests/TestHarness.cpp +18 -0
  118. package/native/tests/test_action_sheet.cpp +157 -0
  119. package/native/tests/test_app_state.cpp +60 -0
  120. package/native/tests/test_background_layers.cpp +281 -0
  121. package/native/tests/test_blend_modes.cpp +79 -0
  122. package/native/tests/test_blobs.cpp +1 -1
  123. package/native/tests/test_device_metrics.cpp +126 -0
  124. package/native/tests/test_filters.cpp +271 -0
  125. package/native/tests/test_font_fitting.cpp +172 -0
  126. package/native/tests/test_font_scaling.cpp +127 -0
  127. package/native/tests/test_font_variants.cpp +84 -0
  128. package/native/tests/test_gradients.cpp +390 -0
  129. package/native/tests/test_image_animation.cpp +105 -0
  130. package/native/tests/test_image_progress.cpp +94 -0
  131. package/native/tests/test_label_registry.cpp +262 -0
  132. package/native/tests/test_legacy_shadow.cpp +165 -0
  133. package/native/tests/test_live_regions.cpp +108 -0
  134. package/native/tests/test_menu_entries.cpp +192 -0
  135. package/native/tests/test_momentum.cpp +45 -0
  136. package/native/tests/test_scroll_bounds.cpp +42 -0
  137. package/native/tests/test_scroll_indicator.cpp +61 -0
  138. package/native/tests/test_scroll_snap.cpp +129 -0
  139. package/native/tests/test_scroll_visible_position.cpp +121 -0
  140. package/native/tests/test_settings_store.cpp +257 -0
  141. package/native/tests/test_shadow_shape.cpp +47 -0
  142. package/native/tests/test_system_locale.cpp +36 -0
  143. package/native/tests/test_text_alignment.cpp +299 -0
  144. package/native/tests/test_text_checking.cpp +31 -0
  145. package/native/tests/test_text_colors.cpp +92 -0
  146. package/native/tests/test_text_decorations.cpp +100 -0
  147. package/native/tests/test_text_selection.cpp +168 -0
  148. package/native/tests/test_text_shadows.cpp +132 -0
  149. package/package.json +4 -2
@@ -0,0 +1,109 @@
1
+ // Which frame an animated image is showing, and when the next one is due.
2
+ //
3
+ // A GIF carries a delay per frame and a loop count, and every platform hands
4
+ // those over differently: ImageIO answers `CGImageSourceGetCount` and a
5
+ // per-frame dictionary, WIC the same through its metadata reader, gdk-pixbuf
6
+ // nothing at all -- it owns the timing itself behind `GdkPixbufAnimationIter`
7
+ // and offers no indexed access to frames. So this file holds the part that is
8
+ // the same wherever the frames come from: the clamp, and the arithmetic from
9
+ // elapsed time to a frame index.
10
+ //
11
+ // The GTK host uses only the clamp, and asks gdk-pixbuf for the rest. It is
12
+ // still worth sharing, because an animation driven by the platform's own clock
13
+ // is one a test has to wait for, and a test that waits for a GIF is a test that
14
+ // is slow and flaky at once. Both hosts advance their animation from an elapsed
15
+ // time the caller supplies, which is what makes "150 milliseconds later" a
16
+ // thing a suite can say.
17
+ //
18
+ // ## The clamp, which is the browsers' rule rather than the format's
19
+ //
20
+ // A GIF may ask for a delay of zero, and a great many do: in the format's own
21
+ // terms that means "as fast as the hardware can", which in 1989 meant a
22
+ // perceptible speed and now means a thousand frames a second. Every browser
23
+ // refuses, and they refuse the same way: a delay at or below 10 milliseconds
24
+ // becomes 100. The number is not arbitrary -- 100ms is the delay the oldest
25
+ // authoring tools wrote by default, so it is what those files were drawn for.
26
+ //
27
+ // Chrome, Firefox and Safari all apply it, which is the reason to copy it
28
+ // rather than invent one: an app author compares against a browser or against
29
+ // React Native on a phone, and the phone is doing this too.
30
+ //
31
+ // ## Loop counts
32
+ //
33
+ // Zero means forever, which is the common case and what the NETSCAPE2.0
34
+ // extension writes for a looping GIF. A positive count means that many passes
35
+ // and then a stop, and the frame it stops on is the last one, not the first:
36
+ // the final frame is what the animation was drawn to end on. A single frame, or
37
+ // no frames at all, is finished before it starts.
38
+
39
+ #pragma once
40
+
41
+ #include <cstddef>
42
+ #include <cstdint>
43
+ #include <vector>
44
+
45
+ namespace basalt {
46
+
47
+ // A delay of 10ms or less means 100ms. See the note above.
48
+ inline unsigned clampedImageFrameDelay(unsigned delayMs) {
49
+ return delayMs <= 10U ? 100U : delayMs;
50
+ }
51
+
52
+ struct ImageAnimationStep {
53
+ // Which frame to show, always a valid index when there is at least one.
54
+ std::size_t frame{0};
55
+ // How long until the frame changes. Zero when nothing more is due, which is
56
+ // what a host uses to stop ticking.
57
+ unsigned nextInMs{0};
58
+ // Nothing more will change: one frame, or the last loop is over.
59
+ bool finished{true};
60
+ };
61
+
62
+ // `delaysMs` is one raw delay per frame, as the file carries it; the clamp is
63
+ // applied here so no caller has to remember to. `loopCount` is zero for
64
+ // forever. `elapsedMs` is how long the image has been animating, which the
65
+ // caller accumulates from its own clock rather than reading one here -- see the
66
+ // note above.
67
+ inline ImageAnimationStep imageAnimationStep(const std::vector<unsigned> &delaysMs,
68
+ unsigned loopCount,
69
+ std::uint64_t elapsedMs) {
70
+ ImageAnimationStep step;
71
+ if (delaysMs.size() < 2) {
72
+ return step;
73
+ }
74
+
75
+ std::uint64_t cycle = 0;
76
+ for (const unsigned delay : delaysMs) {
77
+ cycle += clampedImageFrameDelay(delay);
78
+ }
79
+ if (cycle == 0) {
80
+ return step;
81
+ }
82
+
83
+ // The last frame, left showing. A count that has run out is the only way an
84
+ // animation ends, and it ends on what it was drawn to end on.
85
+ if (loopCount > 0 && elapsedMs >= cycle * loopCount) {
86
+ step.frame = delaysMs.size() - 1;
87
+ return step;
88
+ }
89
+
90
+ std::uint64_t into = elapsedMs % cycle;
91
+ for (std::size_t index = 0; index < delaysMs.size(); index++) {
92
+ const unsigned delay = clampedImageFrameDelay(delaysMs[index]);
93
+ if (into < delay) {
94
+ step.frame = index;
95
+ step.nextInMs = static_cast<unsigned>(delay - into);
96
+ step.finished = false;
97
+ return step;
98
+ }
99
+ into -= delay;
100
+ }
101
+
102
+ // Unreachable: `into` is less than the cycle, and the loop above subtracts
103
+ // the whole cycle. Answering with the last frame rather than asserting,
104
+ // because a wrong frame is better than a dead window.
105
+ step.frame = delaysMs.size() - 1;
106
+ return step;
107
+ }
108
+
109
+ } // namespace basalt
@@ -101,7 +101,10 @@ bool readFile(const std::string &path, std::string *out, std::string *error) {
101
101
 
102
102
  } // namespace
103
103
 
104
- bool fetchImageBytes(const std::string &uri, std::string *out, std::string *error) {
104
+ bool fetchImageBytes(const std::string &uri,
105
+ std::string *out,
106
+ std::string *error,
107
+ const ImageProgress &onProgress) {
105
108
  out->clear();
106
109
 
107
110
  if (uri.empty()) {
@@ -143,6 +146,30 @@ bool fetchImageBytes(const std::string &uri, std::string *out, std::string *erro
143
146
  // Without this, libcurl installs a SIGALRM-based resolver timeout that is
144
147
  // not safe to use off the main thread.
145
148
  curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);
149
+
150
+ // `<Image onProgress>`. Only wired when somebody asked: libcurl's default
151
+ // is no progress callback at all, and turning it on for every fetch would
152
+ // be work for the overwhelming majority of images, which are small and
153
+ // local.
154
+ //
155
+ // XFERINFOFUNCTION rather than PROGRESSFUNCTION, which is the same callback
156
+ // with doubles and deprecated since 7.32. Returning non-zero aborts the
157
+ // transfer, so this returns zero whatever the callback does.
158
+ if (onProgress) {
159
+ curl_easy_setopt(handle, CURLOPT_NOPROGRESS, 0L);
160
+ curl_easy_setopt(handle, CURLOPT_XFERINFODATA, &onProgress);
161
+ curl_easy_setopt(
162
+ handle,
163
+ CURLOPT_XFERINFOFUNCTION,
164
+ +[](void *data, curl_off_t total, curl_off_t now, curl_off_t, curl_off_t) -> int {
165
+ const auto *report = static_cast<const ImageProgress *>(data);
166
+ if (report != nullptr && *report) {
167
+ (*report)(static_cast<long long>(now), static_cast<long long>(total));
168
+ }
169
+ return 0;
170
+ });
171
+ }
172
+
146
173
  const CURLcode result = curl_easy_perform(handle);
147
174
  long status = 0;
148
175
  curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &status);
@@ -11,6 +11,7 @@
11
11
 
12
12
  #pragma once
13
13
 
14
+ #include <functional>
14
15
  #include <string>
15
16
 
16
17
  namespace basalt {
@@ -22,8 +23,23 @@ namespace basalt {
22
23
  // bare paths treated as file paths, which is what a `require()`d asset
23
24
  // looks like once Metro has resolved it in a release bundle
24
25
  //
26
+ // How much of an http fetch has arrived, which is what `<Image onProgress>`
27
+ // reports. `total` is zero when the server sent no `Content-Length`, which is
28
+ // not an error and is why it is passed on rather than turned into a fraction
29
+ // here: an app showing a bar needs to know the difference between "half way"
30
+ // and "no idea".
31
+ //
32
+ // Called from the fetching thread, repeatedly, and never after the fetch has
33
+ // returned. Only an http or https URI reports anything: a file or a data URI
34
+ // arrives whole, and inventing a 0% and a 100% for them would be reporting
35
+ // progress that never existed.
36
+ using ImageProgress = std::function<void(long long loaded, long long total)>;
37
+
25
38
  // Blocking, so call it off the thread that draws. Returns false and fills
26
39
  // `error` on failure, in which case `out` is left empty.
27
- bool fetchImageBytes(const std::string &uri, std::string *out, std::string *error);
40
+ bool fetchImageBytes(const std::string &uri,
41
+ std::string *out,
42
+ std::string *error,
43
+ const ImageProgress &onProgress = {});
28
44
 
29
45
  } // namespace basalt
@@ -0,0 +1,85 @@
1
+ // Which bytes of an image fetch are worth telling JavaScript about.
2
+ //
3
+ // `<Image onProgress>` is a prop an app writes to draw a bar, and the bytes
4
+ // behind it arrive on libcurl's clock: `CURLOPT_XFERINFOFUNCTION` is called
5
+ // roughly every time a socket read returns, which for a megabyte over a fast
6
+ // link is thousands of times. Every one of those would be a Fabric event, a
7
+ // JavaScript call and a React render, for a bar whose width is measured in
8
+ // pixels and cannot show the difference.
9
+ //
10
+ // So the ticks are gated here, on whole percentages: a hundred events over a
11
+ // download, which is more than a bar can resolve and few enough to be free.
12
+ // The gate is in core rather than in each host's mounting manager because all
13
+ // three mean the same thing by it, and because this way it is a thing a test
14
+ // can say something about -- the hosts' half is a lookup and a lambda, and the
15
+ // arithmetic is here.
16
+ //
17
+ // Two cases that are not a percentage, and both are real:
18
+ //
19
+ // **No `Content-Length`.** A chunked response has no total, which is not an
20
+ // error: libcurl reports a growing `loaded` against a zero `total` and the
21
+ // event carries both, so an app can show bytes where it cannot show a
22
+ // fraction. There is nothing to throttle against, so every tick is reported.
23
+ // Turning it into a fraction here -- against the bytes so far, say -- would be
24
+ // inventing a denominator and would read as a bar that is always nearly full.
25
+ //
26
+ // **libcurl's opening tick.** The callback fires once before the response
27
+ // headers have been read, with nothing loaded and nothing known. That one says
28
+ // neither how much has arrived nor how much will, so it is not an event; an app
29
+ // that sees it would show a zero-of-zero bar before the request had even been
30
+ // answered.
31
+
32
+ #pragma once
33
+
34
+ #include <optional>
35
+
36
+ namespace basalt {
37
+
38
+ // One fetch's worth of gating. Belongs to the load it was made for, and is
39
+ // called only from the thread doing that fetch -- which is why it needs no
40
+ // lock: libcurl calls its progress function from the thread inside
41
+ // `curl_easy_perform`, and that is one thread per handle.
42
+ class ImageProgressTicker {
43
+ public:
44
+ // The fraction to report, or nothing when this tick is not worth an event.
45
+ //
46
+ // `loaded` and `total` are bytes, as libcurl counts them. A `total` of zero
47
+ // or less means the server did not say; see the note above.
48
+ std::optional<double> tick(long long loaded, long long total) {
49
+ if (loaded <= 0 && total <= 0) {
50
+ // Nothing arrived and nothing is known. See the note above.
51
+ return std::nullopt;
52
+ }
53
+ if (total <= 0) {
54
+ // Indeterminate: the event still carries `loaded` and a zero `total`,
55
+ // and the fraction is the only thing that cannot be answered.
56
+ return 0.0;
57
+ }
58
+ if (loaded < 0) {
59
+ loaded = 0;
60
+ }
61
+ if (loaded > total) {
62
+ // A server that sent more than it promised. Clamped rather than passed
63
+ // on, so an app drawing `progress * width` cannot overrun its bar.
64
+ loaded = total;
65
+ }
66
+
67
+ const int percent = static_cast<int>(loaded * 100 / total);
68
+ if (percent == reported_) {
69
+ return std::nullopt;
70
+ }
71
+ reported_ = percent;
72
+ return static_cast<double>(loaded) / static_cast<double>(total);
73
+ }
74
+
75
+ // Which whole percentage was last reported, or -1 before the first tick.
76
+ // For the tests; the hosts have no use for it.
77
+ int reportedPercent() const {
78
+ return reported_;
79
+ }
80
+
81
+ private:
82
+ int reported_{-1};
83
+ };
84
+
85
+ } // namespace basalt
@@ -0,0 +1,189 @@
1
+ // Which view a `nativeID` names, and which views are waiting on one.
2
+ //
3
+ // `accessibilityLabelledBy` names other views by their `nativeID` -- "the label
4
+ // for this field is that text over there" -- and nothing else in this project
5
+ // needs to look a view up that way: the registry in core/MountingWalk.h is keyed
6
+ // by tag, and the drag-and-drop code walks *up* from a hit test rather than
7
+ // searching for an id. So this is the missing half, and it is shared because
8
+ // both hosts need exactly the same bookkeeping and the ordering problem below is
9
+ // not host-specific at all.
10
+ //
11
+ // ## The ordering problem
12
+ //
13
+ // A view can name an id that does not exist yet. Fabric mounts in tree order, so
14
+ // a field labelled by the text *after* it is mounted before its label, and a
15
+ // resolution done once at mount time would find nothing and stay wrong. The
16
+ // relation also has to come apart again: a label that is unmounted leaves a
17
+ // dangling reference, and a screen reader following it would read a view that is
18
+ // no longer on screen.
19
+ //
20
+ // So this holds both sides and answers which relations have *changed*, which is
21
+ // what a host applies. Asking after every transaction is cheap and is right
22
+ // whatever order things mounted in.
23
+ //
24
+ // ## Two relations, one index
25
+ //
26
+ // `experimental_accessibilityOrder` asks the same question in a different
27
+ // direction: a parent lists its children's `nativeID`s in the order a screen
28
+ // reader should read them. Same bookkeeping, same ordering problem, same need
29
+ // to report only what changed -- and the same `nativeID` index, which is why it
30
+ // is here rather than in a second class. Two classes would mean two answers to
31
+ // "which view is called that", and they would disagree the first time a view
32
+ // changed its id.
33
+ //
34
+ // Header-only, like core/Backface.h, and free of React Native: it takes tags and
35
+ // strings.
36
+
37
+ #pragma once
38
+
39
+ #include <cstdint>
40
+ #include <string>
41
+ #include <unordered_map>
42
+ #include <utility>
43
+ #include <vector>
44
+
45
+ namespace basalt {
46
+
47
+ class LabelRegistry {
48
+ public:
49
+ using Tag = std::int32_t;
50
+
51
+ // One view's relation, resolved: the tags its `accessibilityLabelledBy` ids
52
+ // name, in the order the app wrote them, and empty when none of them resolve.
53
+ struct Resolved {
54
+ Tag tag;
55
+ std::vector<Tag> labels;
56
+ };
57
+
58
+ // This view's `nativeID`. Empty forgets it, which is what a view that drops
59
+ // the prop means.
60
+ void setNativeId(Tag tag, const std::string &nativeId) {
61
+ const auto previous = nativeIdOf_.find(tag);
62
+ if (previous != nativeIdOf_.end()) {
63
+ if (previous->second == nativeId) {
64
+ return;
65
+ }
66
+ // Only if it is still ours: two views with the same nativeID is an app
67
+ // bug, and the second one to claim it owns it until it goes away.
68
+ const auto owner = tagOfNativeId_.find(previous->second);
69
+ if (owner != tagOfNativeId_.end() && owner->second == tag) {
70
+ tagOfNativeId_.erase(owner);
71
+ }
72
+ nativeIdOf_.erase(previous);
73
+ }
74
+ if (nativeId.empty()) {
75
+ return;
76
+ }
77
+ nativeIdOf_[tag] = nativeId;
78
+ tagOfNativeId_[nativeId] = tag;
79
+ }
80
+
81
+ // This view's `accessibilityLabelledBy`. Empty forgets it.
82
+ void setLabelledBy(Tag tag, std::vector<std::string> ids) {
83
+ if (ids.empty()) {
84
+ labelledBy_.erase(tag);
85
+ return;
86
+ }
87
+ labelledBy_[tag] = std::move(ids);
88
+ }
89
+
90
+ // This view's `experimental_accessibilityOrder`: the ids of its children, in
91
+ // the order they should be read. Empty forgets it.
92
+ void setAccessibilityOrder(Tag tag, std::vector<std::string> ids) {
93
+ if (ids.empty()) {
94
+ order_.erase(tag);
95
+ return;
96
+ }
97
+ order_[tag] = std::move(ids);
98
+ }
99
+
100
+ // A view that has gone away, on either side of either relation.
101
+ void forget(Tag tag) {
102
+ setNativeId(tag, "");
103
+ labelledBy_.erase(tag);
104
+ applied_.erase(tag);
105
+ order_.erase(tag);
106
+ appliedOrder_.erase(tag);
107
+ }
108
+
109
+ // Nothing to ask about: no view wants a relation, and none has one applied.
110
+ //
111
+ // Both halves matter. A host that checked only the first would skip the
112
+ // transaction in which the last relation went away, and the relation it had
113
+ // already applied would stay on the widget for the rest of the screen's life.
114
+ bool empty() const {
115
+ return labelledBy_.empty() && applied_.empty() && order_.empty()
116
+ && appliedOrder_.empty();
117
+ }
118
+
119
+ // The relations whose resolution has changed since the last call, and nothing
120
+ // else: applying an unchanged relation again would tell assistive technology
121
+ // that something happened when nothing did.
122
+ //
123
+ // An id that names no view yet resolves to nothing, and the view is reported
124
+ // again when the id turns up. A relation whose labels have all gone away is
125
+ // reported with an empty list, which is a host's cue to reset it.
126
+ std::vector<Resolved> changes() { return changesOf(labelledBy_, applied_, true); }
127
+
128
+ // The reading orders whose resolution has changed, on the same terms.
129
+ //
130
+ // An id that names nothing is dropped rather than skipping the whole order: a
131
+ // parent that lists four children and has three mounted should read those
132
+ // three in the order it gave, and take the fourth when it arrives.
133
+ std::vector<Resolved> orderChanges() { return changesOf(order_, appliedOrder_, false); }
134
+
135
+ private:
136
+ // `excludeSelf` is the difference between the two relations rather than an
137
+ // option: a view labelled by itself is an app bug worth ignoring, and a
138
+ // parent is never in its own reading order anyway, so for an order the test
139
+ // would only cost a comparison per id.
140
+ std::vector<Resolved> changesOf(
141
+ const std::unordered_map<Tag, std::vector<std::string>> &wanted,
142
+ std::unordered_map<Tag, std::vector<Tag>> &applied,
143
+ bool excludeSelf) {
144
+ std::vector<Resolved> changed;
145
+ for (const auto &[tag, ids] : wanted) {
146
+ std::vector<Tag> labels;
147
+ labels.reserve(ids.size());
148
+ for (const std::string &id : ids) {
149
+ const auto found = tagOfNativeId_.find(id);
150
+ if (found != tagOfNativeId_.end() && (!excludeSelf || found->second != tag)) {
151
+ labels.push_back(found->second);
152
+ }
153
+ }
154
+ const auto previous = applied.find(tag);
155
+ if (previous != applied.end() && previous->second == labels) {
156
+ continue;
157
+ }
158
+ applied[tag] = labels;
159
+ changed.push_back(Resolved{tag, std::move(labels)});
160
+ }
161
+
162
+ // A view that stopped asking: its relation was applied and is no longer
163
+ // wanted, so it is reported empty once and then forgotten.
164
+ for (auto it = applied.begin(); it != applied.end();) {
165
+ if (wanted.find(it->first) != wanted.end()) {
166
+ ++it;
167
+ continue;
168
+ }
169
+ if (!it->second.empty()) {
170
+ changed.push_back(Resolved{it->first, {}});
171
+ }
172
+ it = applied.erase(it);
173
+ }
174
+ return changed;
175
+ }
176
+
177
+ std::unordered_map<Tag, std::string> nativeIdOf_;
178
+ std::unordered_map<std::string, Tag> tagOfNativeId_;
179
+ std::unordered_map<Tag, std::vector<std::string>> labelledBy_;
180
+ // What each view's relation was last reported as, so an unchanged one is not
181
+ // reported twice.
182
+ std::unordered_map<Tag, std::vector<Tag>> applied_;
183
+ // The second relation: which children a view wants read, and what that was
184
+ // last reported as.
185
+ std::unordered_map<Tag, std::vector<std::string>> order_;
186
+ std::unordered_map<Tag, std::vector<Tag>> appliedOrder_;
187
+ };
188
+
189
+ } // namespace basalt
@@ -0,0 +1,90 @@
1
+ // The four iOS shadow props, as the CSS box shadow they describe.
2
+ //
3
+ // `shadowColor`, `shadowOffset`, `shadowOpacity` and `shadowRadius` are React
4
+ // Native's older spelling of a drop shadow, and they are everywhere in real
5
+ // apps: every card, sheet and floating button written before `boxShadow`
6
+ // existed sets them. They are also iOS-only. Android's view config does not
7
+ // carry them, so on Android they do nothing at all and `elevation` is used
8
+ // instead.
9
+ //
10
+ // ## The decision this header is
11
+ //
12
+ // These hosts honour them, rather than ignoring them as Android does. Two
13
+ // reasons. The AppKit host is macOS, where react-native-macos honours them for
14
+ // the same reason its iOS parent does -- the props describe a `CALayer` shadow
15
+ // and the layer is right there. And a cross-platform app that asks for a shadow
16
+ // means the shadow: dropping it on a desktop host would be a deliberate
17
+ // difference from what the app's author saw, which is not what "ignored on this
18
+ // platform" should mean on a platform that can draw it.
19
+ //
20
+ // They are converted here rather than drawn, so both hosts get one shadow list
21
+ // and neither has a second shadow mechanism. What arrives is a `BoxShadow` with
22
+ // CSS's meaning, which both view layers already know how to paint.
23
+ //
24
+ // ## The two conversions that are not identity
25
+ //
26
+ // `shadowRadius` is a `CALayer` blur radius, which is a gaussian standard
27
+ // deviation; CSS's blur-radius is twice that. So the blur doubles, which is the
28
+ // same factor each host already uses in the other direction when it hands a CSS
29
+ // blur to its own compositor -- see the box shadow tests on both.
30
+ //
31
+ // `shadowOpacity` multiplies the colour's own alpha, as `CALayer.shadowOpacity`
32
+ // does on top of `shadowColor`. An opacity of zero is iOS's default and means no
33
+ // shadow, which is why nothing is produced for it.
34
+ //
35
+ // Both props keep their defaults from `BaseViewProps` -- offset (0, -3) and
36
+ // radius 3 -- so a view that sets only `shadowColor` and `shadowOpacity` gets
37
+ // the shadow iOS would give it.
38
+
39
+ #pragma once
40
+
41
+ #include <react/renderer/components/view/BaseViewProps.h>
42
+ #include <react/renderer/graphics/BoxShadow.h>
43
+
44
+ #include <optional>
45
+ #include <vector>
46
+
47
+ namespace basalt {
48
+
49
+ // The shadow the four legacy props ask for, or nothing when they ask for none.
50
+ inline std::optional<facebook::react::BoxShadow> legacyShadow(
51
+ const facebook::react::BaseViewProps &props) {
52
+ if (props.shadowOpacity <= 0.0 || !props.shadowColor) {
53
+ return std::nullopt;
54
+ }
55
+ facebook::react::ColorComponents components =
56
+ facebook::react::colorComponentsFromColor(props.shadowColor);
57
+ if (components.alpha <= 0.0F) {
58
+ return std::nullopt;
59
+ }
60
+ components.alpha *= static_cast<float>(props.shadowOpacity);
61
+ if (components.alpha > 1.0F) {
62
+ components.alpha = 1.0F;
63
+ }
64
+
65
+ facebook::react::BoxShadow shadow;
66
+ shadow.offsetX = props.shadowOffset.width;
67
+ shadow.offsetY = props.shadowOffset.height;
68
+ // A CALayer blur radius is a standard deviation; CSS's is twice one.
69
+ shadow.blurRadius = props.shadowRadius * 2;
70
+ shadow.spreadDistance = 0;
71
+ shadow.color = facebook::react::colorFromComponents(components);
72
+ shadow.inset = false;
73
+ return shadow;
74
+ }
75
+
76
+ // Every shadow a view casts, in CSS's order: first in the list is the one on
77
+ // top. `boxShadow` comes first and the legacy shadow goes behind all of it,
78
+ // which is both what iOS does -- the layer's own shadow is behind the extra
79
+ // layers React Native adds for `boxShadow` -- and the only order where adding a
80
+ // `boxShadow` to an old component cannot be hidden by the shadow it already had.
81
+ inline std::vector<facebook::react::BoxShadow> allShadows(
82
+ const facebook::react::BaseViewProps &props) {
83
+ std::vector<facebook::react::BoxShadow> shadows = props.boxShadow;
84
+ if (const std::optional<facebook::react::BoxShadow> legacy = legacyShadow(props)) {
85
+ shadows.push_back(*legacy);
86
+ }
87
+ return shadows;
88
+ }
89
+
90
+ } // namespace basalt
@@ -0,0 +1,114 @@
1
+ // Which views are live regions, and what each of them last said.
2
+ //
3
+ // `accessibilityLiveRegion` is the prop for a status message: "saved", "three
4
+ // results", "offline". It does not change how a view looks or what it reports
5
+ // when a screen reader lands on it -- it asks for the new text to be *read out*
6
+ // when it changes, without the reader being there. Neither toolkit models that
7
+ // as a property of a view: GTK has `gtk_accessible_announce` and AppKit an
8
+ // `NSAccessibilityAnnouncementRequested` notification, both of which happen at a
9
+ // moment rather than being set.
10
+ //
11
+ // So honouring the prop is change detection, and that is what this holds: the
12
+ // politeness each live region asked for, and the last thing each one said. A
13
+ // host collects a region's current text after a transaction and asks here
14
+ // whether that is news.
15
+ //
16
+ // ## Two rules that are the whole of it
17
+ //
18
+ // **The first text is not news.** Mounting a status line should not read it out:
19
+ // nothing changed, the screen just appeared. So the first text a region is seen
20
+ // with is remembered silently, and only a later difference is announced. Getting
21
+ // this wrong is loud -- every screen with a status message would announce itself
22
+ // on arrival.
23
+ //
24
+ // **The same text twice is not news either.** A view re-renders for every reason
25
+ // under the sun, and React Native re-sends identical props on each mutation; a
26
+ // region that announced on every transaction would talk over everything else in
27
+ // the application.
28
+ //
29
+ // Header-only, like core/LabelRegistry.h, which solves the sibling problem for
30
+ // `accessibilityLabelledBy`. It takes React Native's own politeness enum rather
31
+ // than a copy of it: both mounting managers already know React Native, and a
32
+ // second three-value enum with a mapping either side of it is two more places to
33
+ // put `Polite` where `Assertive` belongs.
34
+
35
+ #pragma once
36
+
37
+ #include <react/renderer/components/view/AccessibilityPrimitives.h>
38
+
39
+ #include <cstdint>
40
+ #include <optional>
41
+ #include <string>
42
+ #include <unordered_map>
43
+ #include <vector>
44
+
45
+ namespace basalt {
46
+
47
+ using LiveRegionPoliteness = facebook::react::AccessibilityLiveRegion;
48
+
49
+ class LiveRegionRegistry {
50
+ public:
51
+ using Tag = std::int32_t;
52
+
53
+ // This view's `accessibilityLiveRegion`. `None` forgets it, which is what a
54
+ // view that drops the prop means.
55
+ void setPoliteness(Tag tag, LiveRegionPoliteness politeness) {
56
+ if (politeness == LiveRegionPoliteness::None) {
57
+ regions_.erase(tag);
58
+ return;
59
+ }
60
+ Region &region = regions_[tag];
61
+ region.politeness = politeness;
62
+ }
63
+
64
+ // A view that has gone away. Its last text goes with it, so a region that
65
+ // comes back is new again rather than compared against a screen that is no
66
+ // longer there.
67
+ void forget(Tag tag) { regions_.erase(tag); }
68
+
69
+ bool empty() const { return regions_.empty(); }
70
+
71
+ // The live regions, in no particular order, for a host to collect text for.
72
+ std::vector<Tag> tags() const {
73
+ std::vector<Tag> tags;
74
+ tags.reserve(regions_.size());
75
+ for (const auto &[tag, region] : regions_) {
76
+ tags.push_back(tag);
77
+ }
78
+ return tags;
79
+ }
80
+
81
+ // What this region should say now, or nothing.
82
+ //
83
+ // Nothing covers three cases that are all ordinary: the view is not a live
84
+ // region, this is the first text it has been seen with, or the text has not
85
+ // changed. Empty text is remembered and never announced -- a region that
86
+ // cleared itself has nothing to read out, and announcing "" is silence with a
87
+ // beat of interruption in front of it.
88
+ std::optional<LiveRegionPoliteness> noticed(Tag tag, const std::string &text) {
89
+ const auto found = regions_.find(tag);
90
+ if (found == regions_.end()) {
91
+ return std::nullopt;
92
+ }
93
+ Region &region = found->second;
94
+ const bool first = !region.said.has_value();
95
+ const bool changed = !first && region.said.value() != text;
96
+ region.said = text;
97
+ if (first || !changed || text.empty()) {
98
+ return std::nullopt;
99
+ }
100
+ return region.politeness;
101
+ }
102
+
103
+ private:
104
+ struct Region {
105
+ LiveRegionPoliteness politeness{LiveRegionPoliteness::Polite};
106
+ // Absent until the region has been seen once, which is what tells a first
107
+ // sighting from a change.
108
+ std::optional<std::string> said;
109
+ };
110
+
111
+ std::unordered_map<Tag, Region> regions_;
112
+ };
113
+
114
+ } // namespace basalt