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,127 @@
|
|
|
1
|
+
// What a fragment's font size works out to. See core/FontScaling.h.
|
|
2
|
+
//
|
|
3
|
+
// Shared because `allowFontScaling` and `maxFontSizeMultiplier` are rules
|
|
4
|
+
// rather than drawing: upstream's clamping is copied from
|
|
5
|
+
// `RCTEffectiveFontSizeMultiplierFromTextAttributes`, and a host reimplementing
|
|
6
|
+
// it would be a host getting it slightly differently.
|
|
7
|
+
|
|
8
|
+
#include "TestHarness.h"
|
|
9
|
+
|
|
10
|
+
#include "FontScaling.h"
|
|
11
|
+
|
|
12
|
+
#include <cmath>
|
|
13
|
+
|
|
14
|
+
using basalt::effectiveFontSize;
|
|
15
|
+
using basalt::effectiveFontSizeMultiplier;
|
|
16
|
+
using facebook::react::TextAttributes;
|
|
17
|
+
|
|
18
|
+
namespace {
|
|
19
|
+
|
|
20
|
+
// Not `near`: that is a legacy macro from <minwindef.h>, which arrives
|
|
21
|
+
// through AttributedString.h on Windows and turns the declaration below into
|
|
22
|
+
// `bool (float, float)`. Every other core test that spells it `near` gets away
|
|
23
|
+
// with it by not including that header.
|
|
24
|
+
bool closeTo(float a, float b) { return std::fabs(a - b) < 0.001F; }
|
|
25
|
+
|
|
26
|
+
} // namespace
|
|
27
|
+
|
|
28
|
+
// Nothing set, and nothing scaling: the size an app asked for.
|
|
29
|
+
TEST(font_scaling_a_plain_fragment_is_its_own_size) {
|
|
30
|
+
TextAttributes attributes;
|
|
31
|
+
attributes.fontSize = 16.0;
|
|
32
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 1.0F), 16.0F));
|
|
33
|
+
// fontSize unset is NaN, which is the host's default rather than zero.
|
|
34
|
+
TextAttributes unsized;
|
|
35
|
+
EXPECT(closeTo(effectiveFontSize(unsized, 13.0F, 1.0F), 13.0F));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The platform's text scale, which is what GTK and Windows publish and macOS
|
|
39
|
+
// does not.
|
|
40
|
+
TEST(font_scaling_the_system_scale_applies) {
|
|
41
|
+
TextAttributes attributes;
|
|
42
|
+
attributes.fontSize = 20.0;
|
|
43
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 1.5F), 30.0F));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The prop an app is likelier to set than any of the others.
|
|
47
|
+
TEST(font_scaling_allow_font_scaling_false_ignores_the_system) {
|
|
48
|
+
TextAttributes attributes;
|
|
49
|
+
attributes.fontSize = 20.0;
|
|
50
|
+
attributes.allowFontScaling = false;
|
|
51
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 1.5F), 20.0F));
|
|
52
|
+
EXPECT(closeTo(effectiveFontSizeMultiplier(attributes, 2.0F), 1.0F));
|
|
53
|
+
|
|
54
|
+
// Unset means scaling, which is React Native's default and not this
|
|
55
|
+
// platform's choice: `allowFontScaling.value_or(true)` is upstream's line.
|
|
56
|
+
TextAttributes unsaid;
|
|
57
|
+
unsaid.fontSize = 20.0;
|
|
58
|
+
EXPECT(closeTo(effectiveFontSize(unsaid, 13.0F, 1.5F), 30.0F));
|
|
59
|
+
TextAttributes allowed;
|
|
60
|
+
allowed.fontSize = 20.0;
|
|
61
|
+
allowed.allowFontScaling = true;
|
|
62
|
+
EXPECT(closeTo(effectiveFontSize(allowed, 13.0F, 1.5F), 30.0F));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A ceiling on growth, which is the other half of the pair.
|
|
66
|
+
TEST(font_scaling_max_font_size_multiplier_is_a_ceiling) {
|
|
67
|
+
TextAttributes attributes;
|
|
68
|
+
attributes.fontSize = 10.0;
|
|
69
|
+
attributes.maxFontSizeMultiplier = 1.2;
|
|
70
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 2.0F), 12.0F));
|
|
71
|
+
// Below the ceiling it changes nothing.
|
|
72
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 1.1F), 11.0F));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Upstream's rule, and the reason it is worth a test of its own: 0 and NaN both
|
|
76
|
+
// mean "no limit", and a limit under 1 is ignored rather than shrinking text.
|
|
77
|
+
TEST(font_scaling_a_limit_under_one_is_no_limit) {
|
|
78
|
+
TextAttributes attributes;
|
|
79
|
+
attributes.fontSize = 10.0;
|
|
80
|
+
attributes.maxFontSizeMultiplier = 0.0;
|
|
81
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 2.0F), 20.0F));
|
|
82
|
+
|
|
83
|
+
attributes.maxFontSizeMultiplier = 0.5;
|
|
84
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 2.0F), 20.0F));
|
|
85
|
+
|
|
86
|
+
TextAttributes unset;
|
|
87
|
+
unset.fontSize = 10.0;
|
|
88
|
+
EXPECT(std::isnan(unset.maxFontSizeMultiplier));
|
|
89
|
+
EXPECT(closeTo(effectiveFontSize(unset, 13.0F, 2.0F), 20.0F));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The prop and the platform multiply, which is the bug this test exists for.
|
|
93
|
+
//
|
|
94
|
+
// The first version of this had the prop win when it was set, which read well
|
|
95
|
+
// and cancelled the platform's scale on every paragraph in an app:
|
|
96
|
+
// `TextAttributes::defaultTextAttributes()` sets `fontSizeMultiplier` to 1
|
|
97
|
+
// rather than to NaN, so "set" is not a thing a fragment can fail to be. The
|
|
98
|
+
// numbers below are the ones that would have passed either way had they been
|
|
99
|
+
// chosen less carefully -- 3 times 1.5 is 4.5, which neither rule produces by
|
|
100
|
+
// accident.
|
|
101
|
+
TEST(font_scaling_a_multiplier_and_the_system_scale_multiply) {
|
|
102
|
+
TextAttributes attributes;
|
|
103
|
+
attributes.fontSize = 10.0;
|
|
104
|
+
attributes.fontSizeMultiplier = 3.0;
|
|
105
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 1.5F), 45.0F));
|
|
106
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 1.0F), 30.0F));
|
|
107
|
+
|
|
108
|
+
// What React Native actually sends, which is the default rather than nothing.
|
|
109
|
+
TextAttributes defaulted;
|
|
110
|
+
defaulted.fontSize = 10.0;
|
|
111
|
+
defaulted.fontSizeMultiplier = 1.0;
|
|
112
|
+
EXPECT(closeTo(effectiveFontSize(defaulted, 13.0F, 1.6F), 16.0F));
|
|
113
|
+
|
|
114
|
+
// And `allowFontScaling={false}` turns both off, as upstream's `else` does.
|
|
115
|
+
attributes.allowFontScaling = false;
|
|
116
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 1.5F), 10.0F));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A setting is not worth trusting into negative or zero territory: text that
|
|
120
|
+
// scales to nothing is worse than text that does not scale.
|
|
121
|
+
TEST(font_scaling_a_scale_that_is_not_positive_is_one) {
|
|
122
|
+
TextAttributes attributes;
|
|
123
|
+
attributes.fontSize = 10.0;
|
|
124
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, 0.0F), 10.0F));
|
|
125
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, -2.0F), 10.0F));
|
|
126
|
+
EXPECT(closeTo(effectiveFontSize(attributes, 13.0F, std::nanf("")), 10.0F));
|
|
127
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// `fontVariant`, as flags and as OpenType tags. See core/FontVariants.h.
|
|
2
|
+
|
|
3
|
+
#include "TestHarness.h"
|
|
4
|
+
|
|
5
|
+
#include "FontVariants.h"
|
|
6
|
+
|
|
7
|
+
#include <sstream>
|
|
8
|
+
#include <string>
|
|
9
|
+
|
|
10
|
+
using basalt::fontFeatureSettings;
|
|
11
|
+
using basalt::fontVariants;
|
|
12
|
+
using basalt::openTypeTag;
|
|
13
|
+
using facebook::react::FontVariant;
|
|
14
|
+
using facebook::react::TextAttributes;
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
TextAttributes with(int mask) {
|
|
19
|
+
TextAttributes attributes;
|
|
20
|
+
attributes.fontVariant = static_cast<FontVariant>(mask);
|
|
21
|
+
return attributes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
} // namespace
|
|
25
|
+
|
|
26
|
+
TEST(font_variants_nothing_asked_is_no_features) {
|
|
27
|
+
TextAttributes attributes;
|
|
28
|
+
EXPECT(fontVariants(attributes).empty());
|
|
29
|
+
EXPECT(fontFeatureSettings(attributes).empty());
|
|
30
|
+
|
|
31
|
+
// And `Default` is the absence of a variant rather than one of them, which
|
|
32
|
+
// matters because React Native spells "no variants" as that rather than as an
|
|
33
|
+
// unset optional when a stylesheet says `fontVariant: []`.
|
|
34
|
+
EXPECT(fontVariants(with(static_cast<int>(FontVariant::Default))).empty());
|
|
35
|
+
EXPECT(fontFeatureSettings(with(static_cast<int>(FontVariant::Default))).empty());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
TEST(font_variants_one_flag_is_one_tag) {
|
|
39
|
+
EXPECT_EQ(fontFeatureSettings(with(static_cast<int>(FontVariant::SmallCaps))),
|
|
40
|
+
std::string("smcp=1"));
|
|
41
|
+
EXPECT_EQ(fontFeatureSettings(with(static_cast<int>(FontVariant::TabularNums))),
|
|
42
|
+
std::string("tnum=1"));
|
|
43
|
+
EXPECT_EQ(fontFeatureSettings(with(static_cast<int>(FontVariant::OldstyleNums))),
|
|
44
|
+
std::string("onum=1"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A bitmask, which is the whole reason this is not a single value: a stylesheet
|
|
48
|
+
// asks for a list and React Native ORs it into one int.
|
|
49
|
+
TEST(font_variants_several_flags_come_back_in_a_fixed_order) {
|
|
50
|
+
const int mask = static_cast<int>(FontVariant::TabularNums)
|
|
51
|
+
| static_cast<int>(FontVariant::SmallCaps)
|
|
52
|
+
| static_cast<int>(FontVariant::StylisticTwo);
|
|
53
|
+
const auto variants = fontVariants(with(mask));
|
|
54
|
+
EXPECT_EQ(variants.size(), std::size_t{3});
|
|
55
|
+
// Lowest bit first, so both hosts apply the same features in the same order.
|
|
56
|
+
EXPECT(variants[0] == FontVariant::SmallCaps);
|
|
57
|
+
EXPECT(variants[1] == FontVariant::TabularNums);
|
|
58
|
+
EXPECT(variants[2] == FontVariant::StylisticTwo);
|
|
59
|
+
EXPECT_EQ(fontFeatureSettings(with(mask)), std::string("smcp=1,tnum=1,ss02=1"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Twenty alternates, and the tag is where an off-by-one would hide: `ss01` is
|
|
63
|
+
// StylisticOne and `ss20` is the last.
|
|
64
|
+
TEST(font_variants_a_stylistic_alternate_is_ss_and_its_number) {
|
|
65
|
+
EXPECT_EQ(std::string(openTypeTag(FontVariant::StylisticOne)), std::string("ss01"));
|
|
66
|
+
EXPECT_EQ(std::string(openTypeTag(FontVariant::StylisticNine)), std::string("ss09"));
|
|
67
|
+
EXPECT_EQ(std::string(openTypeTag(FontVariant::StylisticTen)), std::string("ss10"));
|
|
68
|
+
EXPECT_EQ(std::string(openTypeTag(FontVariant::StylisticTwenty)), std::string("ss20"));
|
|
69
|
+
EXPECT(openTypeTag(FontVariant::Default) == nullptr);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Every flag React Native declares has a tag, which is the check that catches
|
|
73
|
+
// an upstream addition rather than a typo.
|
|
74
|
+
TEST(font_variants_every_flag_has_a_tag) {
|
|
75
|
+
int mask = 0;
|
|
76
|
+
for (int bit = 1; bit <= 25; bit++) {
|
|
77
|
+
mask |= 1 << bit;
|
|
78
|
+
}
|
|
79
|
+
const auto variants = fontVariants(with(mask));
|
|
80
|
+
EXPECT_EQ(variants.size(), std::size_t{25});
|
|
81
|
+
for (const auto variant : variants) {
|
|
82
|
+
EXPECT(openTypeTag(variant) != nullptr);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// The arithmetic behind a CSS gradient: where its line runs and where its stops
|
|
2
|
+
// land.
|
|
3
|
+
//
|
|
4
|
+
// Shared by both hosts, so it is asserted once here rather than twice over a
|
|
5
|
+
// toolkit. What makes it worth asserting at all is that every wrong answer still
|
|
6
|
+
// draws a gradient: a line along the diagonal instead of the spec's
|
|
7
|
+
// perpendicular construction, stops spread evenly instead of pulled forward, a
|
|
8
|
+
// transition hint ignored. See core/Gradients.h.
|
|
9
|
+
|
|
10
|
+
#include "TestHarness.h"
|
|
11
|
+
|
|
12
|
+
#include "Gradients.h"
|
|
13
|
+
|
|
14
|
+
#include <cmath>
|
|
15
|
+
#include <sstream>
|
|
16
|
+
|
|
17
|
+
using basalt::GradientLine;
|
|
18
|
+
using basalt::GradientStop;
|
|
19
|
+
using basalt::gradientAngleForKeyword;
|
|
20
|
+
using basalt::linearGradientLineForAngle;
|
|
21
|
+
using basalt::resolveGradientStops;
|
|
22
|
+
using facebook::react::ColorStop;
|
|
23
|
+
using facebook::react::GradientKeyword;
|
|
24
|
+
using facebook::react::UnitType;
|
|
25
|
+
using facebook::react::ValueUnit;
|
|
26
|
+
|
|
27
|
+
namespace {
|
|
28
|
+
|
|
29
|
+
ColorStop stop(unsigned colour, float position, UnitType unit) {
|
|
30
|
+
ColorStop result;
|
|
31
|
+
result.color = facebook::react::SharedColor(colour);
|
|
32
|
+
result.position = ValueUnit{position, unit};
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ColorStop unpositioned(unsigned colour) {
|
|
37
|
+
return stop(colour, 0.0F, UnitType::Undefined);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A hint: a position with no colour of its own.
|
|
41
|
+
ColorStop hint(float percent) {
|
|
42
|
+
ColorStop result;
|
|
43
|
+
result.position = ValueUnit{percent, UnitType::Percent};
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
constexpr unsigned kRed = 0xFFFF0000;
|
|
48
|
+
constexpr unsigned kBlue = 0xFF0000FF;
|
|
49
|
+
|
|
50
|
+
bool near(float a, float b, float slack = 0.01F) { return std::fabs(a - b) <= slack; }
|
|
51
|
+
|
|
52
|
+
} // namespace
|
|
53
|
+
|
|
54
|
+
// The four right angles, which are the ones an app actually writes and the ones
|
|
55
|
+
// the general construction divides by zero on.
|
|
56
|
+
TEST(gradient_the_right_angles_run_along_the_box) {
|
|
57
|
+
// 0deg is "to top": from the bottom edge to the top one.
|
|
58
|
+
const GradientLine up = linearGradientLineForAngle(0, 100, 50);
|
|
59
|
+
EXPECT(near(up.startY, 50) && near(up.endY, 0));
|
|
60
|
+
EXPECT(near(up.startX, 0) && near(up.endX, 0));
|
|
61
|
+
|
|
62
|
+
// 90deg is to the right, 180 down, 270 left.
|
|
63
|
+
const GradientLine right = linearGradientLineForAngle(90, 100, 50);
|
|
64
|
+
EXPECT(near(right.startX, 0) && near(right.endX, 100));
|
|
65
|
+
const GradientLine down = linearGradientLineForAngle(180, 100, 50);
|
|
66
|
+
EXPECT(near(down.startY, 0) && near(down.endY, 50));
|
|
67
|
+
const GradientLine left = linearGradientLineForAngle(270, 100, 50);
|
|
68
|
+
EXPECT(near(left.startX, 100) && near(left.endX, 0));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 45 degrees on a square: the line runs corner to corner, which is the one case
|
|
72
|
+
// where the diagonal happens to be the right answer.
|
|
73
|
+
TEST(gradient_forty_five_degrees_on_a_square_is_the_diagonal) {
|
|
74
|
+
const GradientLine line = linearGradientLineForAngle(45, 100, 100);
|
|
75
|
+
EXPECT(near(line.startX, 0) && near(line.startY, 100));
|
|
76
|
+
EXPECT(near(line.endX, 100) && near(line.endY, 0));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// And on a box that is not square it is not the diagonal, which is the case that
|
|
80
|
+
// tells the spec's construction from the obvious one.
|
|
81
|
+
//
|
|
82
|
+
// Two properties, both of them the spec's own words rather than a number read
|
|
83
|
+
// off this implementation. The line is W*|sin| + H*|cos| long, which for a wide
|
|
84
|
+
// box at 45 degrees is *shorter* than the diagonal -- 212 against 224 -- so a
|
|
85
|
+
// gradient drawn corner to corner would be stretched. And the perpendicular
|
|
86
|
+
// through the end point passes through the corner the gradient heads for, which
|
|
87
|
+
// is the construction itself: the end point here is outside the box, at
|
|
88
|
+
// (175,-25), and that is right.
|
|
89
|
+
TEST(gradient_forty_five_degrees_on_a_wide_box_is_not_the_diagonal) {
|
|
90
|
+
const GradientLine line = linearGradientLineForAngle(45, 200, 100);
|
|
91
|
+
const float diagonal = std::sqrt(200.0F * 200.0F + 100.0F * 100.0F);
|
|
92
|
+
const float expected =
|
|
93
|
+
(200.0F + 100.0F) * std::sin(static_cast<float>(basalt::kPi) / 4.0F);
|
|
94
|
+
EXPECT(near(line.length(), expected, 0.1F));
|
|
95
|
+
EXPECT(line.length() < diagonal);
|
|
96
|
+
|
|
97
|
+
// Symmetric about the centre, whatever the angle.
|
|
98
|
+
EXPECT(near((line.startX + line.endX) / 2, 100));
|
|
99
|
+
EXPECT(near((line.startY + line.endY) / 2, 50));
|
|
100
|
+
|
|
101
|
+
// (corner - end) is perpendicular to the line's own direction. The corner is
|
|
102
|
+
// the top right one, y counting down, which is where 45 degrees points.
|
|
103
|
+
const float dx = line.endX - line.startX;
|
|
104
|
+
const float dy = line.endY - line.startY;
|
|
105
|
+
EXPECT(near(dx * (200.0F - line.endX) + dy * (0.0F - line.endY), 0, 1.0F));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A corner keyword is not 45 degrees either, for the same reason.
|
|
109
|
+
TEST(gradient_a_corner_keyword_follows_the_box_shape) {
|
|
110
|
+
EXPECT(near(gradientAngleForKeyword(GradientKeyword::ToTopRight, 100, 100), 45));
|
|
111
|
+
// Wide box: the angle to the top right is shallower than 45.
|
|
112
|
+
EXPECT(gradientAngleForKeyword(GradientKeyword::ToTopRight, 200, 100) < 45);
|
|
113
|
+
// Tall box: steeper.
|
|
114
|
+
EXPECT(gradientAngleForKeyword(GradientKeyword::ToTopRight, 100, 200) > 45);
|
|
115
|
+
// To bottom right is past 90, to top left past 270: the quadrants the
|
|
116
|
+
// construction picks its corner from.
|
|
117
|
+
EXPECT(gradientAngleForKeyword(GradientKeyword::ToBottomRight, 100, 100) > 90);
|
|
118
|
+
EXPECT(gradientAngleForKeyword(GradientKeyword::ToTopLeft, 100, 100) > 270);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
TEST(gradient_two_stops_with_no_positions_are_the_ends) {
|
|
122
|
+
const auto stops = resolveGradientStops({unpositioned(kRed), unpositioned(kBlue)}, 100);
|
|
123
|
+
EXPECT_EQ((long)stops.size(), 2L);
|
|
124
|
+
EXPECT(near(stops[0].offset, 0));
|
|
125
|
+
EXPECT(near(stops[1].offset, 1));
|
|
126
|
+
EXPECT(near(stops[0].red, 1) && near(stops[0].blue, 0));
|
|
127
|
+
EXPECT(near(stops[1].blue, 1) && near(stops[1].red, 0));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// A run with no positions is spread evenly between the ones that have them,
|
|
131
|
+
// which is step three of the fixup.
|
|
132
|
+
TEST(gradient_a_run_of_unpositioned_stops_is_spread_evenly) {
|
|
133
|
+
const auto stops = resolveGradientStops(
|
|
134
|
+
{unpositioned(kRed), unpositioned(kBlue), unpositioned(kRed), unpositioned(kBlue)}, 100);
|
|
135
|
+
EXPECT_EQ((long)stops.size(), 4L);
|
|
136
|
+
EXPECT(near(stops[0].offset, 0));
|
|
137
|
+
EXPECT(near(stops[1].offset, 1.0F / 3.0F));
|
|
138
|
+
EXPECT(near(stops[2].offset, 2.0F / 3.0F));
|
|
139
|
+
EXPECT(near(stops[3].offset, 1));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A position in points is a distance along the gradient line, so the same
|
|
143
|
+
// stylesheet means different offsets on a short line and a long one.
|
|
144
|
+
TEST(gradient_a_position_in_points_is_measured_along_the_line) {
|
|
145
|
+
const auto shortLine =
|
|
146
|
+
resolveGradientStops({unpositioned(kRed), stop(kBlue, 25, UnitType::Point)}, 100);
|
|
147
|
+
EXPECT(near(shortLine[1].offset, 0.25F));
|
|
148
|
+
const auto longLine =
|
|
149
|
+
resolveGradientStops({unpositioned(kRed), stop(kBlue, 25, UnitType::Point)}, 200);
|
|
150
|
+
EXPECT(near(longLine[1].offset, 0.125F));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// A position that goes backwards is pulled forward to the one before it, which
|
|
154
|
+
// makes `red 60%, blue 20%` a hard edge at 60% rather than a reversed ramp.
|
|
155
|
+
TEST(gradient_a_backwards_position_becomes_a_hard_edge) {
|
|
156
|
+
const auto stops = resolveGradientStops(
|
|
157
|
+
{stop(kRed, 60, UnitType::Percent), stop(kBlue, 20, UnitType::Percent)}, 100);
|
|
158
|
+
EXPECT_EQ((long)stops.size(), 2L);
|
|
159
|
+
EXPECT(near(stops[0].offset, 0.6F));
|
|
160
|
+
EXPECT(near(stops[1].offset, 0.6F));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A transition hint becomes a run of stops along the spec's curve, and the one
|
|
164
|
+
// at the hint's own position is the midpoint colour -- which is the whole point
|
|
165
|
+
// of a hint, and what tells this apart from ignoring it.
|
|
166
|
+
TEST(gradient_a_transition_hint_moves_the_midpoint_colour) {
|
|
167
|
+
const auto stops = resolveGradientStops({unpositioned(kRed), hint(20), unpositioned(kBlue)}, 100);
|
|
168
|
+
// Nine stops in place of the hint, so eleven.
|
|
169
|
+
EXPECT_EQ((long)stops.size(), 11L);
|
|
170
|
+
|
|
171
|
+
// The colour halfway between red and blue should now be at 20%, not at 50%.
|
|
172
|
+
// Find the stop nearest to half red, half blue.
|
|
173
|
+
size_t midpoint = 0;
|
|
174
|
+
float best = 10.0F;
|
|
175
|
+
for (size_t i = 0; i < stops.size(); i++) {
|
|
176
|
+
const float distance = std::fabs(stops[i].red - 0.5F) + std::fabs(stops[i].blue - 0.5F);
|
|
177
|
+
if (distance < best) {
|
|
178
|
+
best = distance;
|
|
179
|
+
midpoint = i;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
EXPECT(stops[midpoint].offset < 0.3F);
|
|
183
|
+
EXPECT(stops[midpoint].offset > 0.1F);
|
|
184
|
+
|
|
185
|
+
// Offsets never go backwards, whatever the hint does.
|
|
186
|
+
for (size_t i = 1; i < stops.size(); i++) {
|
|
187
|
+
EXPECT(stops[i].offset >= stops[i - 1].offset);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// A hint in the middle is a plain linear ramp, so it is dropped rather than
|
|
192
|
+
// expanded into nine stops that say the same thing.
|
|
193
|
+
TEST(gradient_a_hint_in_the_middle_is_dropped) {
|
|
194
|
+
const auto stops = resolveGradientStops({unpositioned(kRed), hint(50), unpositioned(kBlue)}, 100);
|
|
195
|
+
EXPECT_EQ((long)stops.size(), 2L);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
TEST(gradient_no_stops_is_no_gradient) {
|
|
199
|
+
EXPECT_EQ((long)resolveGradientStops({}, 100).size(), 0L);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The radial gradient's ending shape: where its centre is and how big it is.
|
|
203
|
+
//
|
|
204
|
+
// Six ways to ask for a size and four corners to measure to, and every wrong
|
|
205
|
+
// answer still draws a radial gradient -- which is why this is the half that
|
|
206
|
+
// gets the tests. The box below is deliberately not square, 200 by 100, because
|
|
207
|
+
// a square box makes four of the six answers agree.
|
|
208
|
+
namespace {
|
|
209
|
+
|
|
210
|
+
using basalt::GradientEllipse;
|
|
211
|
+
using basalt::radialGradientEllipse;
|
|
212
|
+
using facebook::react::RadialGradient;
|
|
213
|
+
using facebook::react::RadialGradientShape;
|
|
214
|
+
using facebook::react::RadialGradientSize;
|
|
215
|
+
|
|
216
|
+
RadialGradient radial(RadialGradientShape shape, RadialGradientSize::SizeKeyword size) {
|
|
217
|
+
RadialGradient gradient;
|
|
218
|
+
gradient.shape = shape;
|
|
219
|
+
gradient.size = RadialGradientSize{size};
|
|
220
|
+
return gradient;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// A position as CSS writes it: at most one of each axis, measured from that edge.
|
|
224
|
+
void positionFromLeft(RadialGradient &gradient, float value, UnitType unit) {
|
|
225
|
+
gradient.position.left = ValueUnit{value, unit};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
void positionFromTop(RadialGradient &gradient, float value, UnitType unit) {
|
|
229
|
+
gradient.position.top = ValueUnit{value, unit};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
} // namespace
|
|
233
|
+
|
|
234
|
+
TEST(gradients_a_radial_gradient_is_centred_by_default) {
|
|
235
|
+
const GradientEllipse shape = radialGradientEllipse(
|
|
236
|
+
radial(RadialGradientShape::Ellipse, RadialGradientSize::SizeKeyword::FarthestCorner),
|
|
237
|
+
200.0F,
|
|
238
|
+
100.0F);
|
|
239
|
+
EXPECT_NEAR(shape.centerX, 100.0, 0.01);
|
|
240
|
+
EXPECT_NEAR(shape.centerY, 50.0, 0.01);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Each edge the position can be measured from, since `right: 25%` is not
|
|
244
|
+
// `left: 25%` and an implementation that resolved both the same way would look
|
|
245
|
+
// right exactly half the time.
|
|
246
|
+
TEST(gradients_a_radial_position_is_measured_from_the_edge_it_names) {
|
|
247
|
+
RadialGradient left =
|
|
248
|
+
radial(RadialGradientShape::Circle, RadialGradientSize::SizeKeyword::ClosestSide);
|
|
249
|
+
positionFromLeft(left, 25.0F, UnitType::Percent);
|
|
250
|
+
positionFromTop(left, 10.0F, UnitType::Point);
|
|
251
|
+
const GradientEllipse fromLeft = radialGradientEllipse(left, 200.0F, 100.0F);
|
|
252
|
+
EXPECT_NEAR(fromLeft.centerX, 50.0, 0.01);
|
|
253
|
+
EXPECT_NEAR(fromLeft.centerY, 10.0, 0.01);
|
|
254
|
+
|
|
255
|
+
RadialGradient right =
|
|
256
|
+
radial(RadialGradientShape::Circle, RadialGradientSize::SizeKeyword::ClosestSide);
|
|
257
|
+
right.position.right = ValueUnit{25.0F, UnitType::Percent};
|
|
258
|
+
right.position.bottom = ValueUnit{10.0F, UnitType::Point};
|
|
259
|
+
const GradientEllipse fromRight = radialGradientEllipse(right, 200.0F, 100.0F);
|
|
260
|
+
EXPECT_NEAR(fromRight.centerX, 150.0, 0.01);
|
|
261
|
+
EXPECT_NEAR(fromRight.centerY, 90.0, 0.01);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// `closest-side` and `farthest-side`, which are the two easy ones and still tell
|
|
265
|
+
// an ellipse from a circle: the ellipse takes each axis on its own, and the
|
|
266
|
+
// circle takes the smaller or larger of the two.
|
|
267
|
+
TEST(gradients_a_radial_side_size_takes_each_axis_or_the_smaller_one) {
|
|
268
|
+
const GradientEllipse ellipse = radialGradientEllipse(
|
|
269
|
+
radial(RadialGradientShape::Ellipse, RadialGradientSize::SizeKeyword::ClosestSide),
|
|
270
|
+
200.0F,
|
|
271
|
+
100.0F);
|
|
272
|
+
EXPECT_NEAR(ellipse.radiusX, 100.0, 0.01);
|
|
273
|
+
EXPECT_NEAR(ellipse.radiusY, 50.0, 0.01);
|
|
274
|
+
|
|
275
|
+
// A circle that touches the nearest side, which is the top and bottom here.
|
|
276
|
+
const GradientEllipse circle = radialGradientEllipse(
|
|
277
|
+
radial(RadialGradientShape::Circle, RadialGradientSize::SizeKeyword::ClosestSide),
|
|
278
|
+
200.0F,
|
|
279
|
+
100.0F);
|
|
280
|
+
EXPECT_NEAR(circle.radiusX, 50.0, 0.01);
|
|
281
|
+
EXPECT_NEAR(circle.radiusY, 50.0, 0.01);
|
|
282
|
+
|
|
283
|
+
const GradientEllipse farthest = radialGradientEllipse(
|
|
284
|
+
radial(RadialGradientShape::Circle, RadialGradientSize::SizeKeyword::FarthestSide),
|
|
285
|
+
200.0F,
|
|
286
|
+
100.0F);
|
|
287
|
+
EXPECT_NEAR(farthest.radiusX, 100.0, 0.01);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Off centre, where `closest-side` and `farthest-side` stop agreeing with each
|
|
291
|
+
// other and with the box's half-sizes.
|
|
292
|
+
TEST(gradients_a_radial_side_size_is_measured_from_the_centre) {
|
|
293
|
+
RadialGradient gradient =
|
|
294
|
+
radial(RadialGradientShape::Ellipse, RadialGradientSize::SizeKeyword::ClosestSide);
|
|
295
|
+
positionFromLeft(gradient, 25.0F, UnitType::Percent); // 50 of 200
|
|
296
|
+
positionFromTop(gradient, 25.0F, UnitType::Percent); // 25 of 100
|
|
297
|
+
|
|
298
|
+
const GradientEllipse closest = radialGradientEllipse(gradient, 200.0F, 100.0F);
|
|
299
|
+
EXPECT_NEAR(closest.radiusX, 50.0, 0.01);
|
|
300
|
+
EXPECT_NEAR(closest.radiusY, 25.0, 0.01);
|
|
301
|
+
|
|
302
|
+
gradient.size = RadialGradientSize{RadialGradientSize::SizeKeyword::FarthestSide};
|
|
303
|
+
const GradientEllipse farthest = radialGradientEllipse(gradient, 200.0F, 100.0F);
|
|
304
|
+
EXPECT_NEAR(farthest.radiusX, 150.0, 0.01);
|
|
305
|
+
EXPECT_NEAR(farthest.radiusY, 75.0, 0.01);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// A circle to a corner just reaches it, which is a distance rather than a pair.
|
|
309
|
+
TEST(gradients_a_radial_circle_to_a_corner_reaches_it) {
|
|
310
|
+
const GradientEllipse farthest = radialGradientEllipse(
|
|
311
|
+
radial(RadialGradientShape::Circle, RadialGradientSize::SizeKeyword::FarthestCorner),
|
|
312
|
+
200.0F,
|
|
313
|
+
100.0F);
|
|
314
|
+
// Centred, so every corner is the same distance: hypot(100, 50).
|
|
315
|
+
EXPECT_NEAR(farthest.radiusX, std::hypot(100.0, 50.0), 0.01);
|
|
316
|
+
EXPECT_NEAR(farthest.radiusY, std::hypot(100.0, 50.0), 0.01);
|
|
317
|
+
|
|
318
|
+
RadialGradient closest =
|
|
319
|
+
radial(RadialGradientShape::Circle, RadialGradientSize::SizeKeyword::ClosestCorner);
|
|
320
|
+
positionFromLeft(closest, 50.0F, UnitType::Point);
|
|
321
|
+
positionFromTop(closest, 20.0F, UnitType::Point);
|
|
322
|
+
const GradientEllipse near = radialGradientEllipse(closest, 200.0F, 100.0F);
|
|
323
|
+
// The top left corner is the closest from (50, 20).
|
|
324
|
+
EXPECT_NEAR(near.radiusX, std::hypot(50.0, 20.0), 0.01);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The one that a reimplementation gets wrong: an ellipse to a corner has the
|
|
328
|
+
// aspect ratio of the matching *side* shape, and is then scaled until the corner
|
|
329
|
+
// lies on it. The naive answer -- the corner's own dx and dy as the radii -- is
|
|
330
|
+
// smaller, and looks like a gradient.
|
|
331
|
+
TEST(gradients_a_radial_ellipse_to_a_corner_keeps_the_side_aspect_ratio) {
|
|
332
|
+
const GradientEllipse shape = radialGradientEllipse(
|
|
333
|
+
radial(RadialGradientShape::Ellipse, RadialGradientSize::SizeKeyword::FarthestCorner),
|
|
334
|
+
200.0F,
|
|
335
|
+
100.0F);
|
|
336
|
+
|
|
337
|
+
// farthest-side here is (100, 50), so the ratio is 2. The corner is at
|
|
338
|
+
// (100, 50) from the centre, and a = sqrt(100^2 + 50^2 * 4) = sqrt(20000).
|
|
339
|
+
const double expectedX = std::sqrt(100.0 * 100.0 + 50.0 * 50.0 * 4.0);
|
|
340
|
+
EXPECT_NEAR(shape.radiusX, expectedX, 0.01);
|
|
341
|
+
EXPECT_NEAR(shape.radiusY, expectedX / 2.0, 0.01);
|
|
342
|
+
// Bigger than the naive answer, which is the whole point.
|
|
343
|
+
EXPECT(shape.radiusX > 100.0F);
|
|
344
|
+
// And the corner is on it: (dx/rx)^2 + (dy/ry)^2 is 1.
|
|
345
|
+
const double onIt = (100.0 / shape.radiusX) * (100.0 / shape.radiusX) +
|
|
346
|
+
(50.0 / shape.radiusY) * (50.0 / shape.radiusY);
|
|
347
|
+
EXPECT_NEAR(onIt, 1.0, 0.001);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Explicit radii, where a percentage is of the box's own axis rather than of a
|
|
351
|
+
// diagonal: `radial-gradient(50% 25%, ...)` on a 200x100 box is 100 by 25.
|
|
352
|
+
TEST(gradients_explicit_radial_radii_resolve_against_their_own_axis) {
|
|
353
|
+
RadialGradient gradient;
|
|
354
|
+
gradient.shape = RadialGradientShape::Ellipse;
|
|
355
|
+
gradient.size = RadialGradientSize{RadialGradientSize::Dimensions{
|
|
356
|
+
ValueUnit{50.0F, UnitType::Percent}, ValueUnit{25.0F, UnitType::Percent}}};
|
|
357
|
+
|
|
358
|
+
const GradientEllipse shape = radialGradientEllipse(gradient, 200.0F, 100.0F);
|
|
359
|
+
EXPECT_NEAR(shape.radiusX, 100.0, 0.01);
|
|
360
|
+
EXPECT_NEAR(shape.radiusY, 25.0, 0.01);
|
|
361
|
+
// And in points, which is the other unit a stylesheet can use.
|
|
362
|
+
gradient.size = RadialGradientSize{RadialGradientSize::Dimensions{
|
|
363
|
+
ValueUnit{30.0F, UnitType::Point}, ValueUnit{10.0F, UnitType::Point}}};
|
|
364
|
+
const GradientEllipse points = radialGradientEllipse(gradient, 200.0F, 100.0F);
|
|
365
|
+
EXPECT_NEAR(points.radiusX, 30.0, 0.01);
|
|
366
|
+
EXPECT_NEAR(points.radiusY, 10.0, 0.01);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// The ray a stop's length is resolved against, which is the longer radius. A
|
|
370
|
+
// stop at 50pt on a 100-by-25 ellipse is halfway along the long axis, and the
|
|
371
|
+
// same stop list has to come out the same on all three platforms -- so this is
|
|
372
|
+
// React Native's choice rather than one made here.
|
|
373
|
+
TEST(gradients_radial_stops_resolve_against_the_longer_radius) {
|
|
374
|
+
RadialGradient gradient;
|
|
375
|
+
gradient.shape = RadialGradientShape::Ellipse;
|
|
376
|
+
gradient.size = RadialGradientSize{RadialGradientSize::Dimensions{
|
|
377
|
+
ValueUnit{100.0F, UnitType::Point}, ValueUnit{25.0F, UnitType::Point}}};
|
|
378
|
+
const GradientEllipse shape = radialGradientEllipse(gradient, 200.0F, 100.0F);
|
|
379
|
+
EXPECT_NEAR(shape.rayLength(), 100.0, 0.01);
|
|
380
|
+
|
|
381
|
+
const std::vector<ColorStop> stops = {
|
|
382
|
+
stop(0xFFFF0000, 0.0F, UnitType::Point),
|
|
383
|
+
stop(0xFF0000FF, 50.0F, UnitType::Point),
|
|
384
|
+
};
|
|
385
|
+
const std::vector<GradientStop> resolved = resolveGradientStops(stops, shape.rayLength());
|
|
386
|
+
EXPECT_EQ((int)resolved.size(), 2);
|
|
387
|
+
if (resolved.size() == 2) {
|
|
388
|
+
EXPECT_NEAR(resolved[1].offset, 0.5, 0.01);
|
|
389
|
+
}
|
|
390
|
+
}
|