basalt-core 0.0.0-rc5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/basalt.d.ts.map +1 -1
- package/dist/cli/basalt.js +18 -1
- package/dist/cli/basalt.js.map +1 -1
- package/dist/cli/bundle-cli.js +0 -0
- package/dist/cli/packageApp.d.ts.map +1 -1
- package/dist/cli/packageApp.js +7 -1
- package/dist/cli/packageApp.js.map +1 -1
- package/dist/metro-config.d.ts.map +1 -1
- package/dist/metro-config.js +53 -5
- package/dist/metro-config.js.map +1 -1
- package/dist/src/KeyHandler.d.ts +1 -1
- package/dist/src/KeyHandler.d.ts.map +1 -1
- package/dist/src/KeyHandler.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/overrides/Alert.d.ts +1 -1
- package/dist/src/overrides/Alert.js +1 -1
- package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
- package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
- package/dist/src/overrides/BaseViewConfig.js +21 -3
- package/dist/src/overrides/BaseViewConfig.js.map +1 -1
- package/dist/src/overrides/Button.d.ts +81 -0
- package/dist/src/overrides/Button.d.ts.map +1 -0
- package/dist/src/overrides/Button.js +109 -0
- package/dist/src/overrides/Button.js.map +1 -0
- package/dist/src/overrides/ScrollView.d.ts +25 -2
- package/dist/src/overrides/ScrollView.d.ts.map +1 -1
- package/dist/src/overrides/ScrollView.js +64 -7
- package/dist/src/overrides/ScrollView.js.map +1 -1
- package/dist/src/overrides/Settings.d.ts +81 -0
- package/dist/src/overrides/Settings.d.ts.map +1 -0
- package/dist/src/overrides/Settings.js +120 -0
- package/dist/src/overrides/Settings.js.map +1 -0
- package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
- package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
- package/dist/src/overrides/processDecelerationRate.js +31 -0
- package/dist/src/overrides/processDecelerationRate.js.map +1 -0
- package/dist/src/useContextMenu.d.ts +57 -6
- package/dist/src/useContextMenu.d.ts.map +1 -1
- package/dist/src/useContextMenu.js +34 -1
- package/dist/src/useContextMenu.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/native/CMakeLists.txt +51 -0
- package/native/core/ActionSheet.cpp +75 -0
- package/native/core/ActionSheet.h +90 -0
- package/native/core/AppStateHost.cpp +76 -0
- package/native/core/AppStateHost.h +68 -0
- package/native/core/BackgroundLayers.h +282 -0
- package/native/core/BlendModes.h +67 -0
- package/native/core/CoreModules.cpp +380 -11
- package/native/core/CoreModules.h +140 -4
- package/native/core/CrashHandler.cpp +246 -0
- package/native/core/CrashHandler.h +65 -0
- package/native/core/CursorNames.h +110 -0
- package/native/core/DevMenu.cpp +6 -5
- package/native/core/DeviceMetrics.cpp +87 -0
- package/native/core/DeviceMetrics.h +80 -0
- package/native/core/Filters.h +336 -0
- package/native/core/FontFitting.h +183 -0
- package/native/core/FontScaling.h +153 -0
- package/native/core/FontVariants.h +148 -0
- package/native/core/Gradients.h +523 -0
- package/native/core/ImageAnimation.h +109 -0
- package/native/core/ImageBytes.cpp +28 -1
- package/native/core/ImageBytes.h +17 -1
- package/native/core/ImageProgress.h +85 -0
- package/native/core/LabelRegistry.h +189 -0
- package/native/core/LegacyShadow.h +90 -0
- package/native/core/LiveRegions.h +114 -0
- package/native/core/MenuModule.cpp +35 -4
- package/native/core/MountingWalk.h +125 -2
- package/native/core/PlatformServices.h +187 -3
- package/native/core/SafeAreaComponent.cpp +115 -0
- package/native/core/SafeAreaComponent.h +105 -0
- package/native/core/ScreensComponent.cpp +201 -0
- package/native/core/ScreensComponent.h +216 -0
- package/native/core/ScrollBounds.cpp +8 -0
- package/native/core/ScrollBounds.h +14 -0
- package/native/core/ScrollIndicator.h +67 -0
- package/native/core/ScrollMomentum.cpp +24 -0
- package/native/core/ScrollMomentum.h +14 -0
- package/native/core/ScrollSnap.cpp +121 -12
- package/native/core/ScrollSnap.h +45 -0
- package/native/core/ScrollVisiblePosition.cpp +68 -0
- package/native/core/ScrollVisiblePosition.h +101 -0
- package/native/core/SettingsStore.cpp +199 -0
- package/native/core/SettingsStore.h +127 -0
- package/native/core/ShadowShape.h +38 -0
- package/native/core/SystemLocale.cpp +103 -0
- package/native/core/SystemLocale.h +86 -0
- package/native/core/TestDialog.cpp +45 -6
- package/native/core/TestDialog.h +15 -0
- package/native/core/TextAlignments.h +240 -0
- package/native/core/TextChecking.h +133 -0
- package/native/core/TextColors.h +83 -0
- package/native/core/TextDecorations.h +101 -0
- package/native/core/TextDirection.cpp +242 -0
- package/native/core/TextDirection.h +88 -0
- package/native/core/TextHighlight.h +30 -0
- package/native/core/TextSelection.cpp +97 -0
- package/native/core/TextSelection.h +161 -0
- package/native/core/TextShadows.h +96 -0
- package/native/core/TextVerticalAlign.h +33 -0
- package/native/core/UIManagerAccess.cpp +36 -0
- package/native/core/UIManagerAccess.h +28 -0
- package/native/core/WindowBoundsCache.cpp +58 -1
- package/native/core/WindowControl.h +24 -0
- package/native/core/WritingDirections.h +52 -0
- package/native/core/portability_probe.cpp +7 -0
- package/native/tests/RenamedFontFile.h +161 -0
- package/native/tests/TestHarness.cpp +18 -0
- package/native/tests/test_action_sheet.cpp +157 -0
- package/native/tests/test_app_state.cpp +60 -0
- package/native/tests/test_background_layers.cpp +281 -0
- package/native/tests/test_blend_modes.cpp +79 -0
- package/native/tests/test_blobs.cpp +1 -1
- package/native/tests/test_device_metrics.cpp +126 -0
- package/native/tests/test_filters.cpp +271 -0
- package/native/tests/test_font_fitting.cpp +172 -0
- package/native/tests/test_font_scaling.cpp +127 -0
- package/native/tests/test_font_variants.cpp +84 -0
- package/native/tests/test_gradients.cpp +390 -0
- package/native/tests/test_image_animation.cpp +105 -0
- package/native/tests/test_image_progress.cpp +94 -0
- package/native/tests/test_label_registry.cpp +262 -0
- package/native/tests/test_legacy_shadow.cpp +165 -0
- package/native/tests/test_live_regions.cpp +108 -0
- package/native/tests/test_menu_entries.cpp +192 -0
- package/native/tests/test_momentum.cpp +45 -0
- package/native/tests/test_scroll_bounds.cpp +42 -0
- package/native/tests/test_scroll_indicator.cpp +61 -0
- package/native/tests/test_scroll_snap.cpp +129 -0
- package/native/tests/test_scroll_visible_position.cpp +121 -0
- package/native/tests/test_settings_store.cpp +257 -0
- package/native/tests/test_shadow_shape.cpp +47 -0
- package/native/tests/test_system_locale.cpp +36 -0
- package/native/tests/test_text_alignment.cpp +299 -0
- package/native/tests/test_text_checking.cpp +31 -0
- package/native/tests/test_text_colors.cpp +92 -0
- package/native/tests/test_text_decorations.cpp +100 -0
- package/native/tests/test_text_selection.cpp +168 -0
- package/native/tests/test_text_shadows.cpp +132 -0
- package/package.json +4 -2
|
@@ -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
|