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,157 @@
|
|
|
1
|
+
// The menu an action sheet becomes, and reading an answer back out of it.
|
|
2
|
+
//
|
|
3
|
+
// All of the interesting behaviour is the index arithmetic, which is why it is a
|
|
4
|
+
// function rather than part of the module: `ActionSheetIOS` numbers the array it
|
|
5
|
+
// passed, a menu index counts every entry including the title and the message,
|
|
6
|
+
// and getting that wrong means an app runs the wrong choice -- the quietest
|
|
7
|
+
// possible bug, because every index is a valid index.
|
|
8
|
+
//
|
|
9
|
+
// What is asserted here and nowhere else: a dismissal is not an option, a
|
|
10
|
+
// header cannot be chosen, and the offset is exactly the number of entries the
|
|
11
|
+
// title and message took.
|
|
12
|
+
|
|
13
|
+
#include "TestHarness.h"
|
|
14
|
+
|
|
15
|
+
#include "ActionSheet.h"
|
|
16
|
+
|
|
17
|
+
#include <sstream>
|
|
18
|
+
#include <string>
|
|
19
|
+
|
|
20
|
+
using basalt::ActionSheetMenu;
|
|
21
|
+
using basalt::ActionSheetRequest;
|
|
22
|
+
using basalt::actionSheetChoice;
|
|
23
|
+
using basalt::actionSheetMenu;
|
|
24
|
+
|
|
25
|
+
namespace {
|
|
26
|
+
|
|
27
|
+
ActionSheetRequest threeOptions() {
|
|
28
|
+
ActionSheetRequest request;
|
|
29
|
+
request.options = {"Save", "Discard", "Cancel"};
|
|
30
|
+
request.cancelIndex = 2;
|
|
31
|
+
return request;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
} // namespace
|
|
35
|
+
|
|
36
|
+
TEST(action_sheet_options_with_no_title_start_at_the_top) {
|
|
37
|
+
const ActionSheetRequest request = threeOptions();
|
|
38
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
39
|
+
EXPECT_EQ(menu.offset, 0);
|
|
40
|
+
EXPECT_EQ((long)menu.menu.entries.size(), 3L);
|
|
41
|
+
EXPECT_EQ(menu.menu.entries[0].label, std::string("Save"));
|
|
42
|
+
// Negative is "wherever the pointer is", which is what a sheet with no anchor
|
|
43
|
+
// wants and what MenuRequest already means by it.
|
|
44
|
+
EXPECT(menu.menu.x < 0.0);
|
|
45
|
+
EXPECT(menu.menu.y < 0.0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
TEST(action_sheet_a_title_and_a_message_are_disabled_entries_above_a_separator) {
|
|
49
|
+
ActionSheetRequest request = threeOptions();
|
|
50
|
+
request.title = "Unsaved changes";
|
|
51
|
+
request.message = "Three files have not been written";
|
|
52
|
+
|
|
53
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
54
|
+
EXPECT_EQ(menu.offset, 3);
|
|
55
|
+
EXPECT_EQ((long)menu.menu.entries.size(), 6L);
|
|
56
|
+
EXPECT_EQ(menu.menu.entries[0].label, std::string("Unsaved changes"));
|
|
57
|
+
EXPECT(!menu.menu.entries[0].enabled);
|
|
58
|
+
EXPECT(!menu.menu.entries[1].enabled);
|
|
59
|
+
EXPECT(menu.menu.entries[2].isSeparator());
|
|
60
|
+
EXPECT_EQ(menu.menu.entries[3].label, std::string("Save"));
|
|
61
|
+
EXPECT(menu.menu.entries[3].enabled);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
TEST(action_sheet_a_message_alone_still_takes_a_separator) {
|
|
65
|
+
ActionSheetRequest request = threeOptions();
|
|
66
|
+
request.message = "Three files have not been written";
|
|
67
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
68
|
+
EXPECT_EQ(menu.offset, 2);
|
|
69
|
+
EXPECT(menu.menu.entries[1].isSeparator());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
TEST(action_sheet_disabled_indices_grey_the_option_they_name) {
|
|
73
|
+
ActionSheetRequest request = threeOptions();
|
|
74
|
+
request.disabledIndices = {1};
|
|
75
|
+
request.title = "Unsaved changes";
|
|
76
|
+
|
|
77
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
78
|
+
// Offset 2: a title and its separator. The disabled option is the app's
|
|
79
|
+
// index 1, which is this menu's index 3.
|
|
80
|
+
EXPECT_EQ(menu.offset, 2);
|
|
81
|
+
EXPECT(menu.menu.entries[2].enabled);
|
|
82
|
+
EXPECT(!menu.menu.entries[3].enabled);
|
|
83
|
+
EXPECT(menu.menu.entries[4].enabled);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
TEST(action_sheet_an_option_chosen_is_the_apps_own_index) {
|
|
87
|
+
ActionSheetRequest request = threeOptions();
|
|
88
|
+
request.title = "Unsaved changes";
|
|
89
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
90
|
+
|
|
91
|
+
// A title and its separator, so menu index 2 is the first option -- which the
|
|
92
|
+
// app knows as 0. This is the whole reason the offset is carried rather than
|
|
93
|
+
// recomputed, and the arithmetic is the one thing here that can be silently
|
|
94
|
+
// wrong: every index is a valid index.
|
|
95
|
+
EXPECT_EQ(menu.offset, 2);
|
|
96
|
+
EXPECT(actionSheetChoice(request, menu, 2).has_value());
|
|
97
|
+
EXPECT_EQ(*actionSheetChoice(request, menu, 2), 0);
|
|
98
|
+
EXPECT_EQ(*actionSheetChoice(request, menu, 4), 2);
|
|
99
|
+
// One past the last option, which the menu does not have.
|
|
100
|
+
EXPECT(!actionSheetChoice(request, menu, 5).has_value());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
TEST(action_sheet_a_header_is_not_a_choice) {
|
|
104
|
+
ActionSheetRequest request = threeOptions();
|
|
105
|
+
request.title = "Unsaved changes";
|
|
106
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
107
|
+
// A person cannot click a disabled entry and a script cannot either, but the
|
|
108
|
+
// arithmetic has to refuse it on its own: index 0 minus an offset of 2 is not
|
|
109
|
+
// an option, and reporting -2 to an app would be worse than reporting nothing.
|
|
110
|
+
EXPECT(!actionSheetChoice(request, menu, 0).has_value());
|
|
111
|
+
EXPECT(!actionSheetChoice(request, menu, 1).has_value());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
TEST(action_sheet_an_index_past_the_end_is_not_a_choice) {
|
|
115
|
+
const ActionSheetRequest request = threeOptions();
|
|
116
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
117
|
+
EXPECT(!actionSheetChoice(request, menu, 3).has_value());
|
|
118
|
+
EXPECT(!actionSheetChoice(request, menu, 99).has_value());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
TEST(action_sheet_a_dismissal_is_the_cancel_choice_when_there_is_one) {
|
|
122
|
+
// The one place this platform answers something iOS does not. Escape closing
|
|
123
|
+
// a menu is ordinary where an iPad popover being tapped away is not, and an
|
|
124
|
+
// app that named a cancel choice has already said what to do with it.
|
|
125
|
+
const ActionSheetRequest request = threeOptions();
|
|
126
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
127
|
+
EXPECT(actionSheetChoice(request, menu, -1).has_value());
|
|
128
|
+
EXPECT_EQ(*actionSheetChoice(request, menu, -1), 2);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
TEST(action_sheet_a_dismissal_with_no_cancel_choice_reports_nothing) {
|
|
132
|
+
// Nothing rather than -1: `UIAlertController` calls the callback from a
|
|
133
|
+
// button's handler, so a sheet that goes away without one calls nothing, and
|
|
134
|
+
// an app never has to understand an index that is not an index.
|
|
135
|
+
ActionSheetRequest request = threeOptions();
|
|
136
|
+
request.cancelIndex = -1;
|
|
137
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
138
|
+
EXPECT(!actionSheetChoice(request, menu, -1).has_value());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
TEST(action_sheet_a_cancel_index_the_options_do_not_have_reports_nothing) {
|
|
142
|
+
// An app that passed a stale index. Reporting it would be reporting an index
|
|
143
|
+
// into an array that is too short, which is a crash in somebody's handler.
|
|
144
|
+
ActionSheetRequest request = threeOptions();
|
|
145
|
+
request.cancelIndex = 7;
|
|
146
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
147
|
+
EXPECT(!actionSheetChoice(request, menu, -1).has_value());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
TEST(action_sheet_an_anchor_puts_the_menu_where_the_caller_said) {
|
|
151
|
+
ActionSheetRequest request = threeOptions();
|
|
152
|
+
request.x = 120.0;
|
|
153
|
+
request.y = 240.0;
|
|
154
|
+
const ActionSheetMenu menu = actionSheetMenu(request);
|
|
155
|
+
EXPECT_NEAR(menu.menu.x, 120.0, 0.01);
|
|
156
|
+
EXPECT_NEAR(menu.menu.y, 240.0, 0.01);
|
|
157
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// What `AppState.currentState` says, which was the empty string on every host
|
|
2
|
+
// until 2026-10-10. See core/AppStateHost.h.
|
|
3
|
+
|
|
4
|
+
#include "TestHarness.h"
|
|
5
|
+
|
|
6
|
+
#include "AppStateHost.h"
|
|
7
|
+
|
|
8
|
+
#include <sstream>
|
|
9
|
+
#include <string>
|
|
10
|
+
#include <vector>
|
|
11
|
+
|
|
12
|
+
using basalt::AppStateKind;
|
|
13
|
+
using basalt::appStateFor;
|
|
14
|
+
using basalt::appStateName;
|
|
15
|
+
|
|
16
|
+
TEST(app_state_is_the_two_facts_a_desktop_has) {
|
|
17
|
+
EXPECT(appStateFor(true, true) == AppStateKind::Active);
|
|
18
|
+
// Visible and not frontmost: iOS's `inactive` is a transition, and the
|
|
19
|
+
// desktop analogue is another window being in front -- which is the same
|
|
20
|
+
// thing an app uses it for.
|
|
21
|
+
EXPECT(appStateFor(false, true) == AppStateKind::Inactive);
|
|
22
|
+
// Invisible wins over focused: a minimised window that still holds focus is
|
|
23
|
+
// not something an app should treat as frontmost.
|
|
24
|
+
EXPECT(appStateFor(true, false) == AppStateKind::Background);
|
|
25
|
+
EXPECT(appStateFor(false, false) == AppStateKind::Background);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
TEST(app_state_names_are_react_natives_own) {
|
|
29
|
+
EXPECT(std::string(appStateName(AppStateKind::Active)) == "active");
|
|
30
|
+
EXPECT(std::string(appStateName(AppStateKind::Inactive)) == "inactive");
|
|
31
|
+
EXPECT(std::string(appStateName(AppStateKind::Background)) == "background");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// An application that has drawn a window is active, which is what an app
|
|
35
|
+
// reading `currentState` before any event arrives should be told. Upstream
|
|
36
|
+
// answers the empty string, which is not one of React Native's own states.
|
|
37
|
+
TEST(app_state_starts_active) {
|
|
38
|
+
EXPECT(basalt::currentAppState() == AppStateKind::Active);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
TEST(app_state_tells_the_listener_only_when_it_changes) {
|
|
42
|
+
std::vector<AppStateKind> seen;
|
|
43
|
+
basalt::setAppStateListener([&seen](AppStateKind state) { seen.push_back(state); });
|
|
44
|
+
|
|
45
|
+
basalt::notifyAppState(AppStateKind::Active); // already active
|
|
46
|
+
basalt::notifyAppState(AppStateKind::Inactive);
|
|
47
|
+
basalt::notifyAppState(AppStateKind::Inactive);
|
|
48
|
+
basalt::notifyAppState(AppStateKind::Background);
|
|
49
|
+
basalt::notifyAppState(AppStateKind::Active);
|
|
50
|
+
|
|
51
|
+
EXPECT_EQ((long)seen.size(), 3L);
|
|
52
|
+
EXPECT(seen[0] == AppStateKind::Inactive);
|
|
53
|
+
EXPECT(seen[1] == AppStateKind::Background);
|
|
54
|
+
EXPECT(seen[2] == AppStateKind::Active);
|
|
55
|
+
|
|
56
|
+
// And the state is readable without a listener, which is where
|
|
57
|
+
// `getCurrentAppState` reads it.
|
|
58
|
+
EXPECT(basalt::currentAppState() == AppStateKind::Active);
|
|
59
|
+
basalt::setAppStateListener(nullptr);
|
|
60
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Where a background image goes and how it tiles. See core/BackgroundLayers.h.
|
|
2
|
+
//
|
|
3
|
+
// Every number is specified and every wrong answer still paints a gradient, so
|
|
4
|
+
// these are the spec's own cases: percentages against the area rather than the
|
|
5
|
+
// image, the far edges against the space the image leaves, `round` changing the
|
|
6
|
+
// size before anything tiles, and `space` changing the step and the anchor but
|
|
7
|
+
// not the size.
|
|
8
|
+
//
|
|
9
|
+
// The areas are deliberately different throughout -- a 100x100 border box with a
|
|
10
|
+
// 10pt border, so the padding box is (10,10,80,80) -- because an implementation
|
|
11
|
+
// that used one for both is the mistake this file exists to catch, and it is
|
|
12
|
+
// invisible on a view with no border.
|
|
13
|
+
|
|
14
|
+
#include "TestHarness.h"
|
|
15
|
+
|
|
16
|
+
#include "BackgroundLayers.h"
|
|
17
|
+
|
|
18
|
+
#include <sstream>
|
|
19
|
+
|
|
20
|
+
using basalt::BackgroundArea;
|
|
21
|
+
using basalt::BackgroundLayer;
|
|
22
|
+
using basalt::resolveBackgroundLayer;
|
|
23
|
+
using facebook::react::BackgroundPosition;
|
|
24
|
+
using facebook::react::BackgroundRepeat;
|
|
25
|
+
using facebook::react::BackgroundRepeatStyle;
|
|
26
|
+
using facebook::react::BackgroundSize;
|
|
27
|
+
using facebook::react::BackgroundSizeLengthPercentage;
|
|
28
|
+
using facebook::react::UnitType;
|
|
29
|
+
using facebook::react::ValueUnit;
|
|
30
|
+
|
|
31
|
+
namespace {
|
|
32
|
+
|
|
33
|
+
const BackgroundArea kPainting{0.0F, 0.0F, 100.0F, 100.0F};
|
|
34
|
+
const BackgroundArea kPositioning{10.0F, 10.0F, 80.0F, 80.0F};
|
|
35
|
+
|
|
36
|
+
// React Native's defaults, which are what an app that sets none of the three
|
|
37
|
+
// props sends: auto size, the top left corner, and repeat in both axes.
|
|
38
|
+
BackgroundSize autoSize() { return BackgroundSizeLengthPercentage{}; }
|
|
39
|
+
|
|
40
|
+
BackgroundSize sized(float x, UnitType xUnit, float y, UnitType yUnit) {
|
|
41
|
+
BackgroundSizeLengthPercentage size;
|
|
42
|
+
size.x = ValueUnit{x, xUnit};
|
|
43
|
+
size.y = ValueUnit{y, yUnit};
|
|
44
|
+
return size;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
BackgroundPosition at(float x, UnitType xUnit, float y, UnitType yUnit) {
|
|
48
|
+
BackgroundPosition position;
|
|
49
|
+
position.left = ValueUnit{x, xUnit};
|
|
50
|
+
position.top = ValueUnit{y, yUnit};
|
|
51
|
+
return position;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
BackgroundRepeat repeating(BackgroundRepeatStyle x, BackgroundRepeatStyle y) {
|
|
55
|
+
BackgroundRepeat repeat;
|
|
56
|
+
repeat.x = x;
|
|
57
|
+
repeat.y = y;
|
|
58
|
+
return repeat;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
bool near(float a, float b) { return std::fabs(a - b) < 0.01F; }
|
|
62
|
+
|
|
63
|
+
} // namespace
|
|
64
|
+
|
|
65
|
+
// The defaults, which is the case every view with a gradient and nothing else
|
|
66
|
+
// goes through. The image fills the *padding* box, not the border box, which is
|
|
67
|
+
// the difference from what this platform drew before the props were read.
|
|
68
|
+
TEST(background_the_defaults_fill_the_padding_box) {
|
|
69
|
+
const BackgroundLayer layer = resolveBackgroundLayer(
|
|
70
|
+
kPositioning, kPainting, autoSize(), BackgroundPosition{}, BackgroundRepeat{});
|
|
71
|
+
|
|
72
|
+
EXPECT(near(layer.width, 80.0F));
|
|
73
|
+
EXPECT(near(layer.height, 80.0F));
|
|
74
|
+
// Repeat is React Native's default, so the tile is the image and the pattern
|
|
75
|
+
// is backed up far enough to cover the border box it is clipped to.
|
|
76
|
+
EXPECT(layer.repeatsX);
|
|
77
|
+
EXPECT(layer.repeatsY);
|
|
78
|
+
EXPECT(near(layer.tileWidth, 80.0F));
|
|
79
|
+
EXPECT(near(layer.tileHeight, 80.0F));
|
|
80
|
+
EXPECT(near(layer.tileX, -70.0F));
|
|
81
|
+
EXPECT(near(layer.tileY, -70.0F));
|
|
82
|
+
// The image is drawn at the tile's origin, the tile being the image itself.
|
|
83
|
+
EXPECT(near(layer.x, layer.tileX));
|
|
84
|
+
EXPECT(near(layer.y, layer.tileY));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A size in points and a size in percent, where the percentage is of the
|
|
88
|
+
// positioning area and not of the painting area: 50% of an 80pt padding box is
|
|
89
|
+
// 40, where 50% of the 100pt border box would be 50.
|
|
90
|
+
TEST(background_a_size_resolves_against_the_positioning_area) {
|
|
91
|
+
const BackgroundLayer points = resolveBackgroundLayer(
|
|
92
|
+
kPositioning,
|
|
93
|
+
kPainting,
|
|
94
|
+
sized(30.0F, UnitType::Point, 20.0F, UnitType::Point),
|
|
95
|
+
BackgroundPosition{},
|
|
96
|
+
repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
|
|
97
|
+
EXPECT(near(points.width, 30.0F));
|
|
98
|
+
EXPECT(near(points.height, 20.0F));
|
|
99
|
+
|
|
100
|
+
const BackgroundLayer percent = resolveBackgroundLayer(
|
|
101
|
+
kPositioning,
|
|
102
|
+
kPainting,
|
|
103
|
+
sized(50.0F, UnitType::Percent, 25.0F, UnitType::Percent),
|
|
104
|
+
BackgroundPosition{},
|
|
105
|
+
repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
|
|
106
|
+
EXPECT(near(percent.width, 40.0F));
|
|
107
|
+
EXPECT(near(percent.height, 20.0F));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// `cover` and `contain` come to the area, which is what CSS says for an image
|
|
111
|
+
// with no intrinsic size -- a gradient -- and is what iOS does with them.
|
|
112
|
+
TEST(background_cover_and_contain_are_the_area_for_a_gradient) {
|
|
113
|
+
for (const auto keyword :
|
|
114
|
+
{facebook::react::BackgroundSizeKeyword::Cover, facebook::react::BackgroundSizeKeyword::Contain}) {
|
|
115
|
+
const BackgroundLayer layer = resolveBackgroundLayer(
|
|
116
|
+
kPositioning, kPainting, keyword, BackgroundPosition{}, BackgroundRepeat{});
|
|
117
|
+
EXPECT(near(layer.width, 80.0F));
|
|
118
|
+
EXPECT(near(layer.height, 80.0F));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The position, against the space the image leaves rather than the whole area.
|
|
123
|
+
// A 30pt image in an 80pt area has 50pt of slack, so 50% of it is 25 -- not 40.
|
|
124
|
+
TEST(background_a_position_resolves_against_the_space_the_image_leaves) {
|
|
125
|
+
const BackgroundLayer layer = resolveBackgroundLayer(
|
|
126
|
+
kPositioning,
|
|
127
|
+
kPainting,
|
|
128
|
+
sized(30.0F, UnitType::Point, 30.0F, UnitType::Point),
|
|
129
|
+
at(50.0F, UnitType::Percent, 100.0F, UnitType::Percent),
|
|
130
|
+
repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
|
|
131
|
+
|
|
132
|
+
EXPECT(near(layer.x, 10.0F + 25.0F));
|
|
133
|
+
// 100% of the slack is flush with the far edge of the padding box.
|
|
134
|
+
EXPECT(near(layer.y, 10.0F + 50.0F));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The far edges are not the near ones. `right: 25%` is a quarter of the slack in
|
|
138
|
+
// from the right, which is three quarters along, and an implementation that
|
|
139
|
+
// resolved both the same way would be right exactly half the time.
|
|
140
|
+
TEST(background_the_far_edges_are_measured_from_the_far_side) {
|
|
141
|
+
BackgroundPosition position;
|
|
142
|
+
position.right = ValueUnit{25.0F, UnitType::Percent};
|
|
143
|
+
position.bottom = ValueUnit{10.0F, UnitType::Point};
|
|
144
|
+
|
|
145
|
+
const BackgroundLayer layer = resolveBackgroundLayer(
|
|
146
|
+
kPositioning,
|
|
147
|
+
kPainting,
|
|
148
|
+
sized(40.0F, UnitType::Point, 40.0F, UnitType::Point),
|
|
149
|
+
position,
|
|
150
|
+
repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
|
|
151
|
+
|
|
152
|
+
// 40pt of slack, a quarter of it from the right: 10 + 30.
|
|
153
|
+
EXPECT(near(layer.x, 40.0F));
|
|
154
|
+
// 10pt in from the bottom of the padding box: 10 + 40 - 10.
|
|
155
|
+
EXPECT(near(layer.y, 40.0F));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// `no-repeat` leaves the tile as the painting area, so a host that repeats in
|
|
159
|
+
// both axes at once draws it exactly once.
|
|
160
|
+
TEST(background_no_repeat_is_one_tile_over_the_painting_area) {
|
|
161
|
+
const BackgroundLayer layer = resolveBackgroundLayer(
|
|
162
|
+
kPositioning,
|
|
163
|
+
kPainting,
|
|
164
|
+
sized(30.0F, UnitType::Point, 30.0F, UnitType::Point),
|
|
165
|
+
BackgroundPosition{},
|
|
166
|
+
repeating(BackgroundRepeatStyle::NoRepeat, BackgroundRepeatStyle::NoRepeat));
|
|
167
|
+
|
|
168
|
+
EXPECT(!layer.repeatsX);
|
|
169
|
+
EXPECT(!layer.repeatsY);
|
|
170
|
+
EXPECT(near(layer.tileX, 0.0F));
|
|
171
|
+
EXPECT(near(layer.tileY, 0.0F));
|
|
172
|
+
EXPECT(near(layer.tileWidth, 100.0F));
|
|
173
|
+
EXPECT(near(layer.tileHeight, 100.0F));
|
|
174
|
+
// And the image stays where the position put it.
|
|
175
|
+
EXPECT(near(layer.x, 10.0F));
|
|
176
|
+
EXPECT(near(layer.y, 10.0F));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// One axis each way, which is `repeat-x`: the tile spans the painting area
|
|
180
|
+
// downwards and steps by the image's width across.
|
|
181
|
+
TEST(background_one_axis_can_repeat_while_the_other_does_not) {
|
|
182
|
+
const BackgroundLayer layer = resolveBackgroundLayer(
|
|
183
|
+
kPositioning,
|
|
184
|
+
kPainting,
|
|
185
|
+
sized(20.0F, UnitType::Point, 20.0F, UnitType::Point),
|
|
186
|
+
at(0.0F, UnitType::Point, 30.0F, UnitType::Point),
|
|
187
|
+
repeating(BackgroundRepeatStyle::Repeat, BackgroundRepeatStyle::NoRepeat));
|
|
188
|
+
|
|
189
|
+
EXPECT(layer.repeatsX);
|
|
190
|
+
EXPECT(!layer.repeatsY);
|
|
191
|
+
EXPECT(near(layer.tileWidth, 20.0F));
|
|
192
|
+
EXPECT(near(layer.tileHeight, 100.0F));
|
|
193
|
+
// Backed up to cover the border box: 10 - 20 is -10.
|
|
194
|
+
EXPECT(near(layer.tileX, -10.0F));
|
|
195
|
+
EXPECT(near(layer.tileY, 0.0F));
|
|
196
|
+
// The row sits where the position put it, inside the tile.
|
|
197
|
+
EXPECT(near(layer.y, 40.0F));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// `round` divides the area into a whole number of tiles before anything else
|
|
201
|
+
// happens, because it changes the size: 30pt into 80pt is 2.67 tiles, so the
|
|
202
|
+
// nearest whole number is 3 and the tile becomes 26.67.
|
|
203
|
+
TEST(background_round_divides_the_area_into_whole_tiles) {
|
|
204
|
+
const BackgroundLayer layer = resolveBackgroundLayer(
|
|
205
|
+
kPositioning,
|
|
206
|
+
kPainting,
|
|
207
|
+
sized(30.0F, UnitType::Point, 30.0F, UnitType::Point),
|
|
208
|
+
BackgroundPosition{},
|
|
209
|
+
repeating(BackgroundRepeatStyle::Round, BackgroundRepeatStyle::Round));
|
|
210
|
+
|
|
211
|
+
EXPECT(near(layer.width, 80.0F / 3.0F));
|
|
212
|
+
EXPECT(near(layer.height, 80.0F / 3.0F));
|
|
213
|
+
// And it tiles by that size, with no gap: `round` is `repeat` with the size
|
|
214
|
+
// adjusted.
|
|
215
|
+
EXPECT(near(layer.tileWidth, layer.width));
|
|
216
|
+
EXPECT(layer.repeatsX);
|
|
217
|
+
|
|
218
|
+
// A size that already divides the area is left alone, which is the branch that
|
|
219
|
+
// stops a division by a remainder of zero turning into a wrong size.
|
|
220
|
+
const BackgroundLayer exact = resolveBackgroundLayer(
|
|
221
|
+
kPositioning,
|
|
222
|
+
kPainting,
|
|
223
|
+
sized(40.0F, UnitType::Point, 40.0F, UnitType::Point),
|
|
224
|
+
BackgroundPosition{},
|
|
225
|
+
repeating(BackgroundRepeatStyle::Round, BackgroundRepeatStyle::Round));
|
|
226
|
+
EXPECT(near(exact.width, 40.0F));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// `space` keeps the size and shares the slack between the tiles, and the first
|
|
230
|
+
// tile is flush with the area rather than where the position asked: 30pt into
|
|
231
|
+
// 80pt fits twice with 20pt left over, so the step is 50.
|
|
232
|
+
TEST(background_space_shares_the_slack_between_tiles) {
|
|
233
|
+
const BackgroundLayer layer = resolveBackgroundLayer(
|
|
234
|
+
kPositioning,
|
|
235
|
+
kPainting,
|
|
236
|
+
sized(30.0F, UnitType::Point, 30.0F, UnitType::Point),
|
|
237
|
+
at(50.0F, UnitType::Percent, 50.0F, UnitType::Percent),
|
|
238
|
+
repeating(BackgroundRepeatStyle::Space, BackgroundRepeatStyle::Space));
|
|
239
|
+
|
|
240
|
+
EXPECT(near(layer.width, 30.0F));
|
|
241
|
+
EXPECT(near(layer.tileWidth, 50.0F));
|
|
242
|
+
EXPECT(layer.repeatsX);
|
|
243
|
+
// Anchored on the area, not on the position: 10 backed up by one step is -40.
|
|
244
|
+
EXPECT(near(layer.tileX, -40.0F));
|
|
245
|
+
EXPECT(near(layer.x, layer.tileX));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// `space` with one tile has no slack to share, so it stops repeating -- which is
|
|
249
|
+
// the case that would otherwise divide by zero.
|
|
250
|
+
TEST(background_space_with_one_tile_does_not_repeat) {
|
|
251
|
+
const BackgroundLayer layer = resolveBackgroundLayer(
|
|
252
|
+
kPositioning,
|
|
253
|
+
kPainting,
|
|
254
|
+
sized(60.0F, UnitType::Point, 60.0F, UnitType::Point),
|
|
255
|
+
BackgroundPosition{},
|
|
256
|
+
repeating(BackgroundRepeatStyle::Space, BackgroundRepeatStyle::Space));
|
|
257
|
+
|
|
258
|
+
EXPECT(!layer.repeatsX);
|
|
259
|
+
EXPECT(!layer.repeatsY);
|
|
260
|
+
EXPECT(near(layer.width, 60.0F));
|
|
261
|
+
EXPECT(near(layer.x, 10.0F));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Nothing to paint, which both hosts have to be told rather than left to divide
|
|
265
|
+
// by it: a view mid-layout has no size at all.
|
|
266
|
+
TEST(background_an_empty_area_is_nothing_to_paint) {
|
|
267
|
+
const BackgroundArea nothing{0.0F, 0.0F, 0.0F, 0.0F};
|
|
268
|
+
EXPECT(resolveBackgroundLayer(nothing, kPainting, autoSize(), BackgroundPosition{},
|
|
269
|
+
BackgroundRepeat{})
|
|
270
|
+
.empty());
|
|
271
|
+
EXPECT(resolveBackgroundLayer(kPositioning, nothing, autoSize(), BackgroundPosition{},
|
|
272
|
+
BackgroundRepeat{})
|
|
273
|
+
.empty());
|
|
274
|
+
// And a size of zero, which a stylesheet can ask for.
|
|
275
|
+
EXPECT(resolveBackgroundLayer(kPositioning,
|
|
276
|
+
kPainting,
|
|
277
|
+
sized(0.0F, UnitType::Point, 10.0F, UnitType::Point),
|
|
278
|
+
BackgroundPosition{},
|
|
279
|
+
BackgroundRepeat{})
|
|
280
|
+
.empty());
|
|
281
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The `mixBlendMode` keyword table, against React Native's own parser.
|
|
2
|
+
//
|
|
3
|
+
// core/BlendModes.h is seventeen lines of switch, which is seventeen chances to
|
|
4
|
+
// return the wrong neighbour's name -- and a wrong name is not a build error on
|
|
5
|
+
// either host: it is a view that blends with the wrong mode, or stops blending.
|
|
6
|
+
//
|
|
7
|
+
// So the assertion is a round trip rather than a list compared with itself.
|
|
8
|
+
// React Native parses these keywords with `blendModeFromString`, which is what
|
|
9
|
+
// turns the app's style into the enum in the first place, so feeding the name
|
|
10
|
+
// back through it has to give the value it came from. Nothing in this test
|
|
11
|
+
// agrees with the table by construction.
|
|
12
|
+
|
|
13
|
+
#include "TestHarness.h"
|
|
14
|
+
|
|
15
|
+
#include "BlendModes.h"
|
|
16
|
+
|
|
17
|
+
#include <set>
|
|
18
|
+
#include <sstream>
|
|
19
|
+
#include <string>
|
|
20
|
+
|
|
21
|
+
using facebook::react::BlendMode;
|
|
22
|
+
using facebook::react::blendModeFromString;
|
|
23
|
+
|
|
24
|
+
namespace {
|
|
25
|
+
|
|
26
|
+
// The enum is dense and PlusLighter is its last member, which is what makes a
|
|
27
|
+
// loop over it legitimate: a mode added upstream lands inside this range and the
|
|
28
|
+
// count below is what notices.
|
|
29
|
+
constexpr int kModeCount = static_cast<int>(BlendMode::PlusLighter) + 1;
|
|
30
|
+
|
|
31
|
+
} // namespace
|
|
32
|
+
|
|
33
|
+
TEST(blend_modes_every_mode_round_trips_through_react_natives_parser) {
|
|
34
|
+
for (int i = 0; i < kModeCount; i++) {
|
|
35
|
+
const BlendMode mode = static_cast<BlendMode>(i);
|
|
36
|
+
const char *name = basalt::blendModeName(mode);
|
|
37
|
+
if (mode == BlendMode::Normal) {
|
|
38
|
+
// `normal` is the default and asks for nothing, so it crosses as nothing.
|
|
39
|
+
EXPECT(name == nullptr);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
EXPECT(name != nullptr);
|
|
43
|
+
if (name == nullptr) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const std::optional<BlendMode> parsed = blendModeFromString(name);
|
|
47
|
+
EXPECT(parsed.has_value());
|
|
48
|
+
EXPECT(parsed.has_value() && *parsed == mode);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// And no two modes share a name, which the round trip above would catch only if
|
|
53
|
+
// the parser were injective -- it is, but this says the thing directly, and it is
|
|
54
|
+
// the failure a copy-paste in a sixteen-line table actually produces.
|
|
55
|
+
TEST(blend_modes_no_two_modes_share_a_name) {
|
|
56
|
+
std::set<std::string> seen;
|
|
57
|
+
int named = 0;
|
|
58
|
+
for (int i = 0; i < kModeCount; i++) {
|
|
59
|
+
const char *name = basalt::blendModeName(static_cast<BlendMode>(i));
|
|
60
|
+
if (name == nullptr) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
named++;
|
|
64
|
+
seen.insert(name);
|
|
65
|
+
}
|
|
66
|
+
EXPECT_EQ(named, kModeCount - 1);
|
|
67
|
+
EXPECT_EQ(static_cast<int>(seen.size()), named);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The count, so that a mode added upstream is a failing test here rather than a
|
|
71
|
+
// view that silently does not blend. CSS has sixteen separable and non-separable
|
|
72
|
+
// blend modes plus `normal`, and React Native carries all seventeen.
|
|
73
|
+
TEST(blend_modes_there_are_seventeen_of_them) {
|
|
74
|
+
EXPECT_EQ(kModeCount, 17);
|
|
75
|
+
// The ends of the enum, so a reordering upstream is not silently fine either.
|
|
76
|
+
EXPECT(basalt::blendModeName(BlendMode::Multiply) != nullptr);
|
|
77
|
+
EXPECT(std::string(basalt::blendModeName(BlendMode::Multiply)) == "multiply");
|
|
78
|
+
EXPECT(std::string(basalt::blendModeName(BlendMode::PlusLighter)) == "plus-lighter");
|
|
79
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// What is *not* tested here is the module: createFromParts and readAsText take
|
|
7
7
|
// jsi values, which need a runtime, and the thing worth proving about them --
|
|
8
|
-
// that a Blob round-trips through JavaScript -- is proven by
|
|
8
|
+
// that a Blob round-trips through JavaScript -- is proven by e2e/blob.js running
|
|
9
9
|
// on both hosts. This covers the half underneath, where an off-by-one lives.
|
|
10
10
|
|
|
11
11
|
#include "TestHarness.h"
|