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,148 @@
1
+ // `fontVariant`, as OpenType feature tags.
2
+ //
3
+ // React Native's `FontVariant` is a bitmask of twenty-five flags: small caps,
4
+ // four number styles, and twenty stylistic alternates. Nothing read it, so
5
+ // `fontVariant: ['small-caps']` did nothing on any desktop.
6
+ //
7
+ // ## Why the tags are here and the rest is not
8
+ //
9
+ // A feature tag is the portable half. OpenType names each of these with four
10
+ // characters -- `smcp`, `onum`, `lnum`, `tnum`, `pnum`, `ss01` to `ss20` -- and
11
+ // two of the three toolkits take exactly that: Pango through
12
+ // `pango_attr_font_features_new`, DirectWrite through `DWRITE_FONT_FEATURE_TAG`.
13
+ //
14
+ // Core Text does not. It takes Apple's older AAT selectors, a type and a
15
+ // selector number per feature, which is what upstream's iOS half uses in
16
+ // `RCTFontFeatures` and what the AppKit host copies. So the flags are walked
17
+ // here, in one order, and each host turns a flag into its own vocabulary. The
18
+ // alternative -- tags in core and a tag-to-AAT table in AppKit -- would be a
19
+ // translation of a translation.
20
+
21
+ #pragma once
22
+
23
+ #include <react/renderer/attributedstring/TextAttributes.h>
24
+
25
+ #include <string>
26
+ #include <vector>
27
+
28
+ namespace basalt {
29
+
30
+ // The flags an app asked for, lowest bit first, so both hosts apply features in
31
+ // the same order. `Default` is not in the list: it is the absence of a variant
32
+ // rather than one of them.
33
+ inline std::vector<facebook::react::FontVariant> fontVariants(
34
+ const facebook::react::TextAttributes &textAttributes) {
35
+ using facebook::react::FontVariant;
36
+
37
+ std::vector<FontVariant> variants;
38
+ if (!textAttributes.fontVariant.has_value()) {
39
+ return variants;
40
+ }
41
+ const int mask = static_cast<int>(*textAttributes.fontVariant);
42
+
43
+ static const FontVariant kAll[] = {
44
+ FontVariant::SmallCaps, FontVariant::OldstyleNums,
45
+ FontVariant::LiningNums, FontVariant::TabularNums,
46
+ FontVariant::ProportionalNums, FontVariant::StylisticOne,
47
+ FontVariant::StylisticTwo, FontVariant::StylisticThree,
48
+ FontVariant::StylisticFour, FontVariant::StylisticFive,
49
+ FontVariant::StylisticSix, FontVariant::StylisticSeven,
50
+ FontVariant::StylisticEight, FontVariant::StylisticNine,
51
+ FontVariant::StylisticTen, FontVariant::StylisticEleven,
52
+ FontVariant::StylisticTwelve, FontVariant::StylisticThirteen,
53
+ FontVariant::StylisticFourteen, FontVariant::StylisticFifteen,
54
+ FontVariant::StylisticSixteen, FontVariant::StylisticSeventeen,
55
+ FontVariant::StylisticEighteen, FontVariant::StylisticNineteen,
56
+ FontVariant::StylisticTwenty,
57
+ };
58
+ for (const FontVariant variant : kAll) {
59
+ if ((mask & static_cast<int>(variant)) != 0) {
60
+ variants.push_back(variant);
61
+ }
62
+ }
63
+ return variants;
64
+ }
65
+
66
+ // The OpenType tag for one flag, or nullptr for one that has none.
67
+ //
68
+ // The four number styles and small caps are the standard tags; a stylistic
69
+ // alternate is `ssNN`, which is why they are a run rather than a table.
70
+ inline const char *openTypeTag(facebook::react::FontVariant variant) {
71
+ using facebook::react::FontVariant;
72
+ switch (variant) {
73
+ case FontVariant::SmallCaps:
74
+ return "smcp";
75
+ case FontVariant::OldstyleNums:
76
+ return "onum";
77
+ case FontVariant::LiningNums:
78
+ return "lnum";
79
+ case FontVariant::TabularNums:
80
+ return "tnum";
81
+ case FontVariant::ProportionalNums:
82
+ return "pnum";
83
+ case FontVariant::StylisticOne:
84
+ return "ss01";
85
+ case FontVariant::StylisticTwo:
86
+ return "ss02";
87
+ case FontVariant::StylisticThree:
88
+ return "ss03";
89
+ case FontVariant::StylisticFour:
90
+ return "ss04";
91
+ case FontVariant::StylisticFive:
92
+ return "ss05";
93
+ case FontVariant::StylisticSix:
94
+ return "ss06";
95
+ case FontVariant::StylisticSeven:
96
+ return "ss07";
97
+ case FontVariant::StylisticEight:
98
+ return "ss08";
99
+ case FontVariant::StylisticNine:
100
+ return "ss09";
101
+ case FontVariant::StylisticTen:
102
+ return "ss10";
103
+ case FontVariant::StylisticEleven:
104
+ return "ss11";
105
+ case FontVariant::StylisticTwelve:
106
+ return "ss12";
107
+ case FontVariant::StylisticThirteen:
108
+ return "ss13";
109
+ case FontVariant::StylisticFourteen:
110
+ return "ss14";
111
+ case FontVariant::StylisticFifteen:
112
+ return "ss15";
113
+ case FontVariant::StylisticSixteen:
114
+ return "ss16";
115
+ case FontVariant::StylisticSeventeen:
116
+ return "ss17";
117
+ case FontVariant::StylisticEighteen:
118
+ return "ss18";
119
+ case FontVariant::StylisticNineteen:
120
+ return "ss19";
121
+ case FontVariant::StylisticTwenty:
122
+ return "ss20";
123
+ case FontVariant::Default:
124
+ return nullptr;
125
+ }
126
+ return nullptr;
127
+ }
128
+
129
+ // Every flag an app asked for, as the comma-separated list Pango and
130
+ // DirectWrite take: `smcp=1,tnum=1`. Empty when nothing asked.
131
+ inline std::string fontFeatureSettings(
132
+ const facebook::react::TextAttributes &textAttributes) {
133
+ std::string settings;
134
+ for (const auto variant : fontVariants(textAttributes)) {
135
+ const char *const tag = openTypeTag(variant);
136
+ if (tag == nullptr) {
137
+ continue;
138
+ }
139
+ if (!settings.empty()) {
140
+ settings += ",";
141
+ }
142
+ settings += tag;
143
+ settings += "=1";
144
+ }
145
+ return settings;
146
+ }
147
+
148
+ } // namespace basalt
@@ -0,0 +1,523 @@
1
+ // CSS gradients, resolved to the points and stops a toolkit wants.
2
+ //
3
+ // `backgroundImage` is where React Native put CSS gradients, so
4
+ // `linear-gradient(135deg, #f00, #00f)` in a stylesheet arrives as a
5
+ // `LinearGradient` with a direction and a list of colour stops, and every host
6
+ // ignored it. GSK has a linear-gradient node and Core Graphics has `CGGradient`,
7
+ // and both want the same two things: the end points of the gradient line in the
8
+ // view's own pixels, and stops with an offset from 0 to 1.
9
+ //
10
+ // ## Why this is shared and why it is a port
11
+ //
12
+ // Neither half is obvious and both are specified. The gradient line's end points
13
+ // come from CSS's rule that the corners of the box map to the ends of the line,
14
+ // which is a perpendicular-bisector construction rather than "the diagonal"; the
15
+ // stops come from the colour-stop fixup in css-images-4, which fills in missing
16
+ // positions, clamps positions that go backwards, and turns a transition hint into
17
+ // a run of interpolated stops. Getting either subtly wrong gives a gradient that
18
+ // looks like a gradient.
19
+ //
20
+ // So both are ported from React Native's own iOS implementation --
21
+ // `RCTLinearGradient.mm` and `RCTGradientUtils.mm`, which in turn follow
22
+ // Chromium -- and shared between the two hosts, so that the same stylesheet is
23
+ // the same picture on both desktops and on iOS. A host that did its own
24
+ // arithmetic would be plausible and different.
25
+ //
26
+ // Header-only, like core/Backface.h: the mounting managers are what include it,
27
+ // and they already know React Native.
28
+
29
+ #pragma once
30
+
31
+ #include <react/renderer/graphics/Color.h>
32
+ #include <react/renderer/graphics/LinearGradient.h>
33
+ #include <react/renderer/graphics/RadialGradient.h>
34
+ #include <react/renderer/graphics/ValueUnit.h>
35
+
36
+ #include <algorithm>
37
+ #include <cmath>
38
+ #include <optional>
39
+ #include <vector>
40
+
41
+ namespace basalt {
42
+
43
+ // Not the POSIX M_PI, which MSVC's <cmath> only defines behind
44
+ // `_USE_MATH_DEFINES`. That flag is applied to React Native's own targets in
45
+ // core/cmake/ReactNativeCore.cmake and deliberately not to this project's
46
+ // sources, and reaching for it here cost a red Windows build: the GTK and AppKit
47
+ // halves compiled and the shared header did not.
48
+ inline constexpr double kPi = 3.14159265358979323846;
49
+
50
+ // One stop, in the terms a toolkit takes: an offset along the gradient line and
51
+ // a straight sRGB colour.
52
+ struct GradientStop {
53
+ float offset;
54
+ float red;
55
+ float green;
56
+ float blue;
57
+ float alpha;
58
+ };
59
+
60
+ // The gradient line, in the view's own pixels, y pointing down. Both hosts count
61
+ // y that way -- GTK natively, AppKit because these views are flipped -- so the
62
+ // numbers go straight through.
63
+ struct GradientLine {
64
+ float startX;
65
+ float startY;
66
+ float endX;
67
+ float endY;
68
+
69
+ float length() const {
70
+ const float dx = endX - startX;
71
+ const float dy = endY - startY;
72
+ return std::sqrt(dx * dx + dy * dy);
73
+ }
74
+ };
75
+
76
+ // The angle a corner keyword means for a box of this size.
77
+ //
78
+ // Not 45 degrees: `to top right` points at the corner only for a square. CSS
79
+ // says the gradient line is perpendicular to the line joining the two
80
+ // neighbouring corners, which is what makes a gradient to a corner of a wide box
81
+ // shallower than one to a corner of a tall box.
82
+ inline float gradientAngleForKeyword(facebook::react::GradientKeyword keyword,
83
+ float width,
84
+ float height) {
85
+ const double degrees = 180.0 / kPi;
86
+ switch (keyword) {
87
+ case facebook::react::GradientKeyword::ToTopRight:
88
+ return static_cast<float>(90.0 - std::atan(width / height) * degrees);
89
+ case facebook::react::GradientKeyword::ToBottomRight:
90
+ return static_cast<float>(std::atan(width / height) * degrees + 90.0);
91
+ case facebook::react::GradientKeyword::ToTopLeft:
92
+ return static_cast<float>(std::atan(width / height) * degrees + 270.0);
93
+ case facebook::react::GradientKeyword::ToBottomLeft:
94
+ return static_cast<float>(std::atan(height / width) * degrees + 180.0);
95
+ }
96
+ return 180.0F;
97
+ }
98
+
99
+ // Where the gradient line starts and ends for an angle, in CSS's convention: 0
100
+ // degrees points up and they run clockwise, so 90 is to the right.
101
+ //
102
+ // The construction is the one in the spec and in Chromium: take the corner the
103
+ // gradient is heading towards, drop a perpendicular from it onto the gradient
104
+ // line through the centre, and that is the end point. The four right angles are
105
+ // special-cased because the slope is zero or infinite there.
106
+ inline GradientLine linearGradientLineForAngle(float angle, float width, float height) {
107
+ double degrees = std::fmod(static_cast<double>(angle), 360.0);
108
+ if (degrees < 0) {
109
+ degrees += 360.0;
110
+ }
111
+
112
+ if (degrees == 0.0) {
113
+ return GradientLine{0.0F, height, 0.0F, 0.0F};
114
+ }
115
+ if (degrees == 90.0) {
116
+ return GradientLine{0.0F, 0.0F, width, 0.0F};
117
+ }
118
+ if (degrees == 180.0) {
119
+ return GradientLine{0.0F, 0.0F, 0.0F, height};
120
+ }
121
+ if (degrees == 270.0) {
122
+ return GradientLine{width, 0.0F, 0.0F, 0.0F};
123
+ }
124
+
125
+ const double radians = (90.0 - degrees) * kPi / 180.0;
126
+ const double slope = std::tan(radians);
127
+ const double perpendicular = -1.0 / slope;
128
+ const double halfWidth = width / 2.0;
129
+ const double halfHeight = height / 2.0;
130
+
131
+ double cornerX = halfWidth;
132
+ double cornerY = halfHeight;
133
+ if (degrees < 90.0) {
134
+ cornerX = halfWidth;
135
+ cornerY = halfHeight;
136
+ } else if (degrees < 180.0) {
137
+ cornerX = halfWidth;
138
+ cornerY = -halfHeight;
139
+ } else if (degrees < 270.0) {
140
+ cornerX = -halfWidth;
141
+ cornerY = -halfHeight;
142
+ } else {
143
+ cornerX = -halfWidth;
144
+ cornerY = halfHeight;
145
+ }
146
+
147
+ const double intercept = cornerY - perpendicular * cornerX;
148
+ const double endX = intercept / (slope - perpendicular);
149
+ const double endY = perpendicular * endX + intercept;
150
+
151
+ return GradientLine{
152
+ static_cast<float>(halfWidth - endX),
153
+ static_cast<float>(halfHeight + endY),
154
+ static_cast<float>(halfWidth + endX),
155
+ static_cast<float>(halfHeight - endY),
156
+ };
157
+ }
158
+
159
+ inline GradientLine linearGradientLine(const facebook::react::LinearGradient &gradient,
160
+ float width,
161
+ float height) {
162
+ if (std::holds_alternative<facebook::react::Float>(gradient.direction)) {
163
+ const auto angle = static_cast<float>(std::get<facebook::react::Float>(gradient.direction));
164
+ return linearGradientLineForAngle(angle, width, height);
165
+ }
166
+ const auto keyword = std::get<facebook::react::GradientKeyword>(gradient.direction);
167
+ return linearGradientLineForAngle(gradientAngleForKeyword(keyword, width, height), width, height);
168
+ }
169
+
170
+ // CSS's radial gradient, resolved against the box: where its centre is and how
171
+ // big its ending shape is.
172
+ //
173
+ // The geometry is all specified and none of it is obvious, which is why this is
174
+ // a port rather than an implementation: `RadiusToSide`, `EllipseRadius` and
175
+ // `RadiusToCorner` below are React Native's own
176
+ // `React/Fabric/Utils/RCTRadialGradient.mm`, which follows css-images-3. A host
177
+ // that worked out its own ending shape would be plausible and different, and the
178
+ // same stylesheet would be three pictures.
179
+ struct GradientEllipse {
180
+ float centerX;
181
+ float centerY;
182
+ float radiusX;
183
+ float radiusY;
184
+
185
+ // What a stop's length is resolved against. CSS calls it the gradient ray, and
186
+ // for an ellipse React Native's iOS half uses the longer radius, so this does
187
+ // too: a stop at 10pt means the same distance on all three platforms.
188
+ float rayLength() const { return std::max(radiusX, radiusY); }
189
+ };
190
+
191
+ namespace detail {
192
+
193
+ // The ending shape that meets the nearest or the farthest side. A circle takes
194
+ // the smaller or larger of the two, which is what makes `closest-side` on a wide
195
+ // box a circle that touches the top and bottom rather than the left and right.
196
+ inline GradientEllipse radiusToSide(float centerX,
197
+ float centerY,
198
+ float width,
199
+ float height,
200
+ bool isCircle,
201
+ facebook::react::RadialGradientSize::SizeKeyword size) {
202
+ using SizeKeyword = facebook::react::RadialGradientSize::SizeKeyword;
203
+ const float fromLeft = centerX;
204
+ const float fromTop = centerY;
205
+ const float fromRight = width - centerX;
206
+ const float fromBottom = height - centerY;
207
+
208
+ float radiusX = 0.0F;
209
+ float radiusY = 0.0F;
210
+ if (size == SizeKeyword::ClosestSide) {
211
+ radiusX = std::min(fromLeft, fromRight);
212
+ radiusY = std::min(fromTop, fromBottom);
213
+ } else {
214
+ radiusX = std::max(fromLeft, fromRight);
215
+ radiusY = std::max(fromTop, fromBottom);
216
+ }
217
+
218
+ if (isCircle) {
219
+ const float radius =
220
+ size == SizeKeyword::ClosestSide ? std::min(radiusX, radiusY) : std::max(radiusX, radiusY);
221
+ return GradientEllipse{centerX, centerY, radius, radius};
222
+ }
223
+ return GradientEllipse{centerX, centerY, radiusX, radiusY};
224
+ }
225
+
226
+ // The ellipse of a given aspect ratio that passes through one point:
227
+ // (x/a)^2 + (y/b)^2 = 1 with b = a / ratio.
228
+ inline GradientEllipse ellipseThrough(
229
+ float centerX, float centerY, float offsetX, float offsetY, float aspectRatio) {
230
+ if (aspectRatio == 0.0F || std::isinf(aspectRatio) || std::isnan(aspectRatio)) {
231
+ return GradientEllipse{centerX, centerY, 0.0F, 0.0F};
232
+ }
233
+ const float a = std::sqrt(offsetX * offsetX + offsetY * offsetY * aspectRatio * aspectRatio);
234
+ return GradientEllipse{centerX, centerY, a, a / aspectRatio};
235
+ }
236
+
237
+ // The ending shape that meets the nearest or the farthest corner. A circle just
238
+ // reaches it; an ellipse reaches it with the aspect ratio of the matching side
239
+ // shape, which is the spec's rule and the part a reimplementation gets wrong.
240
+ inline GradientEllipse radiusToCorner(float centerX,
241
+ float centerY,
242
+ float width,
243
+ float height,
244
+ bool isCircle,
245
+ facebook::react::RadialGradientSize::SizeKeyword keyword) {
246
+ using SizeKeyword = facebook::react::RadialGradientSize::SizeKeyword;
247
+ const float cornerX[4] = {0.0F, width, width, 0.0F};
248
+ const float cornerY[4] = {0.0F, 0.0F, height, height};
249
+
250
+ const bool closest = keyword == SizeKeyword::ClosestCorner;
251
+ int chosen = 0;
252
+ float distance = std::hypot(centerX - cornerX[0], centerY - cornerY[0]);
253
+ for (int i = 1; i < 4; i++) {
254
+ const float candidate = std::hypot(centerX - cornerX[i], centerY - cornerY[i]);
255
+ if (closest ? candidate < distance : candidate > distance) {
256
+ distance = candidate;
257
+ chosen = i;
258
+ }
259
+ }
260
+
261
+ if (isCircle) {
262
+ return GradientEllipse{centerX, centerY, distance, distance};
263
+ }
264
+
265
+ const GradientEllipse side = radiusToSide(
266
+ centerX,
267
+ centerY,
268
+ width,
269
+ height,
270
+ false,
271
+ closest ? SizeKeyword::ClosestSide : SizeKeyword::FarthestSide);
272
+ return ellipseThrough(centerX,
273
+ centerY,
274
+ cornerX[chosen] - centerX,
275
+ cornerY[chosen] - centerY,
276
+ side.radiusY == 0.0F ? 0.0F : side.radiusX / side.radiusY);
277
+ }
278
+
279
+ } // namespace detail
280
+
281
+ // The whole ending shape: the centre from the position, the radii from the size.
282
+ //
283
+ // The centre defaults to the middle of the box, and a position names at most one
284
+ // of each axis -- `left` or `right`, `top` or `bottom` -- so the other is
285
+ // measured from the opposite edge. The size defaults to `farthest-corner`, which
286
+ // is CSS's default and also what an unparsed size falls back to.
287
+ inline GradientEllipse radialGradientEllipse(const facebook::react::RadialGradient &gradient,
288
+ float width,
289
+ float height) {
290
+ using SizeKeyword = facebook::react::RadialGradientSize::SizeKeyword;
291
+ float centerX = width / 2.0F;
292
+ float centerY = height / 2.0F;
293
+ if (gradient.position.left) {
294
+ centerX = gradient.position.left->resolve(width);
295
+ } else if (gradient.position.right) {
296
+ centerX = width - gradient.position.right->resolve(width);
297
+ }
298
+ if (gradient.position.top) {
299
+ centerY = gradient.position.top->resolve(height);
300
+ } else if (gradient.position.bottom) {
301
+ centerY = height - gradient.position.bottom->resolve(height);
302
+ }
303
+
304
+ const bool isCircle = gradient.shape == facebook::react::RadialGradientShape::Circle;
305
+
306
+ if (std::holds_alternative<facebook::react::RadialGradientSize::Dimensions>(
307
+ gradient.size.value)) {
308
+ const auto &dimensions =
309
+ std::get<facebook::react::RadialGradientSize::Dimensions>(gradient.size.value);
310
+ const float radiusX = dimensions.x.resolve(width);
311
+ const float radiusY = dimensions.y.resolve(height);
312
+ if (isCircle) {
313
+ // The larger of the two, which is what React Native's iOS half does with
314
+ // a circle given two explicit radii. CSS says a circle takes one length,
315
+ // so this is a case the parser should not produce.
316
+ const float radius = std::max(radiusX, radiusY);
317
+ return GradientEllipse{centerX, centerY, radius, radius};
318
+ }
319
+ return GradientEllipse{centerX, centerY, radiusX, radiusY};
320
+ }
321
+
322
+ SizeKeyword keyword = SizeKeyword::FarthestCorner;
323
+ if (std::holds_alternative<SizeKeyword>(gradient.size.value)) {
324
+ keyword = std::get<SizeKeyword>(gradient.size.value);
325
+ }
326
+ if (keyword == SizeKeyword::ClosestSide || keyword == SizeKeyword::FarthestSide) {
327
+ return detail::radiusToSide(centerX, centerY, width, height, isCircle, keyword);
328
+ }
329
+ return detail::radiusToCorner(centerX, centerY, width, height, isCircle, keyword);
330
+ }
331
+
332
+ namespace detail {
333
+
334
+ // A stop's position as a fraction of the line, or nothing when the stylesheet
335
+ // left it out. Points are resolved against the line's length, which is why the
336
+ // length has to be known before the stops are.
337
+ inline std::optional<float> stopPosition(const facebook::react::ValueUnit &position,
338
+ float lineLength) {
339
+ switch (position.unit) {
340
+ case facebook::react::UnitType::Point:
341
+ return lineLength > 0 ? static_cast<float>(position.value) / lineLength : 0.0F;
342
+ case facebook::react::UnitType::Percent:
343
+ return static_cast<float>(position.value) / 100.0F;
344
+ case facebook::react::UnitType::Undefined:
345
+ break;
346
+ }
347
+ return std::nullopt;
348
+ }
349
+
350
+ struct PendingStop {
351
+ facebook::react::SharedColor color;
352
+ std::optional<float> position;
353
+ };
354
+
355
+ inline bool nearly(float a, float b) { return std::fabs(a - b) < 1e-6F; }
356
+
357
+ // A transition hint -- a position in the stop list with no colour of its own,
358
+ // `linear-gradient(red, 40%, blue)` -- becomes a run of ordinary stops whose
359
+ // colours follow the curve the spec gives: the weighting is
360
+ // t^(ln(0.5)/ln(hint)), which is what puts the midpoint colour at the hint.
361
+ //
362
+ // Nine stops, which is what React Native's iOS half emits, so the same
363
+ // stylesheet produces the same ramp there and here. A hint exactly in the middle
364
+ // is dropped instead, that being a plain linear ramp already.
365
+ inline std::vector<PendingStop> expandHints(const std::vector<PendingStop> &original) {
366
+ std::vector<PendingStop> stops(original);
367
+ if (original.size() < 3) {
368
+ return stops;
369
+ }
370
+ int offsetIndex = 0;
371
+ for (size_t i = 1; i + 1 < original.size(); i++) {
372
+ if (original[i].color) {
373
+ continue;
374
+ }
375
+ const auto at = static_cast<size_t>(static_cast<int>(i) + offsetIndex);
376
+ if (at < 1 || at + 1 >= stops.size()) {
377
+ continue;
378
+ }
379
+ const float left = stops[at - 1].position.value_or(0.0F);
380
+ const float right = stops[at + 1].position.value_or(1.0F);
381
+ const float hint = stops[at].position.value_or(0.0F);
382
+ const float leftDistance = hint - left;
383
+ const float rightDistance = right - hint;
384
+ const float total = right - left;
385
+ const facebook::react::SharedColor leftColor = stops[at - 1].color;
386
+ const facebook::react::SharedColor rightColor = stops[at + 1].color;
387
+
388
+ if (nearly(leftDistance, rightDistance)) {
389
+ stops.erase(stops.begin() + static_cast<long>(at));
390
+ offsetIndex--;
391
+ continue;
392
+ }
393
+ if (nearly(leftDistance, 0.0F)) {
394
+ stops[at].color = rightColor;
395
+ continue;
396
+ }
397
+ if (nearly(rightDistance, 0.0F)) {
398
+ stops[at].color = leftColor;
399
+ continue;
400
+ }
401
+
402
+ std::vector<PendingStop> added;
403
+ added.reserve(9);
404
+ if (leftDistance > rightDistance) {
405
+ for (int step = 0; step < 7; step++) {
406
+ added.push_back(PendingStop{
407
+ {}, left + leftDistance * ((7.0F + static_cast<float>(step)) / 13.0F)});
408
+ }
409
+ added.push_back(PendingStop{{}, hint + rightDistance / 3.0F});
410
+ added.push_back(PendingStop{{}, hint + rightDistance * 2.0F / 3.0F});
411
+ } else {
412
+ added.push_back(PendingStop{{}, left + leftDistance / 3.0F});
413
+ added.push_back(PendingStop{{}, left + leftDistance * 2.0F / 3.0F});
414
+ for (int step = 0; step < 7; step++) {
415
+ added.push_back(
416
+ PendingStop{{}, hint + rightDistance * (static_cast<float>(step) / 13.0F)});
417
+ }
418
+ }
419
+
420
+ const auto leftComponents = facebook::react::colorComponentsFromColor(leftColor);
421
+ const auto rightComponents = facebook::react::colorComponentsFromColor(rightColor);
422
+ const double logRatio = std::log(0.5) / std::log(leftDistance / total);
423
+ for (auto &stop : added) {
424
+ const float relative = (stop.position.value() - left) / total;
425
+ const double weight = std::pow(static_cast<double>(relative), logRatio);
426
+ if (!std::isfinite(weight)) {
427
+ continue;
428
+ }
429
+ const auto mix = [weight](float from, float to) {
430
+ return static_cast<float>(from + (to - from) * weight);
431
+ };
432
+ stop.color = facebook::react::colorFromComponents(facebook::react::ColorComponents{
433
+ .red = mix(leftComponents.red, rightComponents.red),
434
+ .green = mix(leftComponents.green, rightComponents.green),
435
+ .blue = mix(leftComponents.blue, rightComponents.blue),
436
+ .alpha = mix(leftComponents.alpha, rightComponents.alpha),
437
+ });
438
+ }
439
+
440
+ stops.erase(stops.begin() + static_cast<long>(at));
441
+ stops.insert(stops.begin() + static_cast<long>(at), added.begin(), added.end());
442
+ offsetIndex += 8;
443
+ }
444
+ return stops;
445
+ }
446
+
447
+ } // namespace detail
448
+
449
+ // The stops as a toolkit wants them: every one with an offset, in order.
450
+ //
451
+ // The fixup in css-images-4, in its own three steps. A first stop with no
452
+ // position is at 0 and a last one at 1; a position that goes backwards is pulled
453
+ // up to the largest before it, so `red 60%, blue 20%` is a hard edge rather than
454
+ // a reversal; and a run with no positions is spread evenly between its
455
+ // neighbours. Then the transition hints are expanded.
456
+ inline std::vector<GradientStop> resolveGradientStops(
457
+ const std::vector<facebook::react::ColorStop> &stops,
458
+ float lineLength) {
459
+ if (stops.empty()) {
460
+ return {};
461
+ }
462
+
463
+ std::vector<detail::PendingStop> pending(stops.size());
464
+ bool anyMissing = false;
465
+ float maxSoFar = detail::stopPosition(stops[0].position, lineLength).value_or(0.0F);
466
+
467
+ for (size_t i = 0; i < stops.size(); i++) {
468
+ auto position = detail::stopPosition(stops[i].position, lineLength);
469
+ if (!position.has_value()) {
470
+ if (i == 0) {
471
+ position = 0.0F;
472
+ } else if (i == stops.size() - 1) {
473
+ position = 1.0F;
474
+ }
475
+ }
476
+ if (position.has_value()) {
477
+ const float clamped = std::max(position.value(), maxSoFar);
478
+ pending[i] = detail::PendingStop{stops[i].color, clamped};
479
+ maxSoFar = clamped;
480
+ } else {
481
+ pending[i] = detail::PendingStop{stops[i].color, std::nullopt};
482
+ anyMissing = true;
483
+ }
484
+ }
485
+
486
+ if (anyMissing) {
487
+ size_t lastDefined = 0;
488
+ for (size_t i = 1; i < pending.size(); i++) {
489
+ if (!pending[i].position.has_value()) {
490
+ continue;
491
+ }
492
+ const size_t gap = i - lastDefined - 1;
493
+ if (gap > 0 && pending[lastDefined].position.has_value()) {
494
+ const float from = pending[lastDefined].position.value();
495
+ const float step = (pending[i].position.value() - from) / static_cast<float>(gap + 1);
496
+ for (size_t j = 1; j <= gap; j++) {
497
+ pending[lastDefined + j].position = from + step * static_cast<float>(j);
498
+ }
499
+ }
500
+ lastDefined = i;
501
+ }
502
+ }
503
+
504
+ std::vector<GradientStop> resolved;
505
+ for (const auto &stop : detail::expandHints(pending)) {
506
+ // A stop React Native could not parse a colour for is dropped rather than
507
+ // painted black, the same judgement the shadows make.
508
+ if (!stop.color || !stop.position.has_value()) {
509
+ continue;
510
+ }
511
+ const auto components = facebook::react::colorComponentsFromColor(stop.color);
512
+ resolved.push_back(GradientStop{
513
+ std::clamp(stop.position.value(), 0.0F, 1.0F),
514
+ components.red,
515
+ components.green,
516
+ components.blue,
517
+ components.alpha,
518
+ });
519
+ }
520
+ return resolved;
521
+ }
522
+
523
+ } // namespace basalt