basalt-core 0.0.0-rc5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/desktop.d.ts.map +1 -1
- package/dist/cli/desktop.js +53 -4
- package/dist/cli/desktop.js.map +1 -1
- 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 +85 -1
- package/native/cmake/Skia.cmake +29 -6
- 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/SkiaViewComponent.cpp +11 -0
- package/native/core/SkiaViewComponent.h +50 -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,108 @@
|
|
|
1
|
+
// Noticing that a status message changed, which is what
|
|
2
|
+
// `accessibilityLiveRegion` asks for.
|
|
3
|
+
//
|
|
4
|
+
// The prop is not a property of a view on either desktop -- GTK announces at a
|
|
5
|
+
// moment, AppKit posts a notification -- so honouring it is change detection,
|
|
6
|
+
// and the two rules that make it bearable rather than deafening are asserted
|
|
7
|
+
// here. See core/LiveRegions.h.
|
|
8
|
+
|
|
9
|
+
#include "TestHarness.h"
|
|
10
|
+
|
|
11
|
+
#include "LiveRegions.h"
|
|
12
|
+
|
|
13
|
+
#include <sstream>
|
|
14
|
+
|
|
15
|
+
using basalt::LiveRegionPoliteness;
|
|
16
|
+
using basalt::LiveRegionRegistry;
|
|
17
|
+
|
|
18
|
+
// Mounting a status line is not news: nothing changed, the screen appeared. A
|
|
19
|
+
// host that announced here would have every screen with a status message read
|
|
20
|
+
// itself out on arrival.
|
|
21
|
+
TEST(live_regions_the_first_text_is_not_announced) {
|
|
22
|
+
LiveRegionRegistry registry;
|
|
23
|
+
registry.setPoliteness(10, LiveRegionPoliteness::Polite);
|
|
24
|
+
|
|
25
|
+
EXPECT(!registry.noticed(10, "Saved").has_value());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
TEST(live_regions_a_change_is_announced_with_the_politeness_asked_for) {
|
|
29
|
+
LiveRegionRegistry registry;
|
|
30
|
+
registry.setPoliteness(10, LiveRegionPoliteness::Polite);
|
|
31
|
+
registry.noticed(10, "Saving");
|
|
32
|
+
|
|
33
|
+
const auto polite = registry.noticed(10, "Saved");
|
|
34
|
+
EXPECT(polite.has_value());
|
|
35
|
+
EXPECT(polite.value() == LiveRegionPoliteness::Polite);
|
|
36
|
+
|
|
37
|
+
registry.setPoliteness(11, LiveRegionPoliteness::Assertive);
|
|
38
|
+
registry.noticed(11, "");
|
|
39
|
+
const auto assertive = registry.noticed(11, "Offline");
|
|
40
|
+
EXPECT(assertive.has_value());
|
|
41
|
+
EXPECT(assertive.value() == LiveRegionPoliteness::Assertive);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A view re-renders for every reason under the sun, and React Native re-sends
|
|
45
|
+
// identical props on each mutation. A region that announced every time would
|
|
46
|
+
// talk over the rest of the application.
|
|
47
|
+
TEST(live_regions_the_same_text_twice_is_not_announced) {
|
|
48
|
+
LiveRegionRegistry registry;
|
|
49
|
+
registry.setPoliteness(10, LiveRegionPoliteness::Polite);
|
|
50
|
+
registry.noticed(10, "Saving");
|
|
51
|
+
EXPECT(registry.noticed(10, "Saved").has_value());
|
|
52
|
+
|
|
53
|
+
EXPECT(!registry.noticed(10, "Saved").has_value());
|
|
54
|
+
EXPECT(!registry.noticed(10, "Saved").has_value());
|
|
55
|
+
// And a change after that is news again.
|
|
56
|
+
EXPECT(registry.noticed(10, "Saved just now").has_value());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A region that cleared itself has nothing to read out. Announcing an empty
|
|
60
|
+
// string is silence with a beat of interruption in front of it.
|
|
61
|
+
TEST(live_regions_clearing_the_text_announces_nothing) {
|
|
62
|
+
LiveRegionRegistry registry;
|
|
63
|
+
registry.setPoliteness(10, LiveRegionPoliteness::Polite);
|
|
64
|
+
registry.noticed(10, "Saved");
|
|
65
|
+
EXPECT(!registry.noticed(10, "").has_value());
|
|
66
|
+
// But the clearing was remembered, so the text coming back is a change.
|
|
67
|
+
EXPECT(registry.noticed(10, "Saved").has_value());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A view that is not a live region never announces, whatever its text does.
|
|
71
|
+
TEST(live_regions_a_view_without_the_prop_is_silent) {
|
|
72
|
+
LiveRegionRegistry registry;
|
|
73
|
+
EXPECT(!registry.noticed(10, "Saving").has_value());
|
|
74
|
+
EXPECT(!registry.noticed(10, "Saved").has_value());
|
|
75
|
+
|
|
76
|
+
registry.setPoliteness(10, LiveRegionPoliteness::Polite);
|
|
77
|
+
registry.setPoliteness(10, LiveRegionPoliteness::None);
|
|
78
|
+
EXPECT(!registry.noticed(10, "Saved again").has_value());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A region that is unmounted and comes back is new again, rather than compared
|
|
82
|
+
// against the text of a screen that is no longer there.
|
|
83
|
+
TEST(live_regions_a_region_that_goes_away_forgets_what_it_said) {
|
|
84
|
+
LiveRegionRegistry registry;
|
|
85
|
+
registry.setPoliteness(10, LiveRegionPoliteness::Polite);
|
|
86
|
+
registry.noticed(10, "Saved");
|
|
87
|
+
|
|
88
|
+
registry.forget(10);
|
|
89
|
+
registry.setPoliteness(10, LiveRegionPoliteness::Polite);
|
|
90
|
+
EXPECT(!registry.noticed(10, "Offline").has_value());
|
|
91
|
+
EXPECT(registry.noticed(10, "Saved").has_value());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The hosts ask `empty()` before doing any work per transaction, collecting a
|
|
95
|
+
// region's text being a walk of its subtree.
|
|
96
|
+
TEST(live_regions_a_registry_with_no_regions_is_empty) {
|
|
97
|
+
LiveRegionRegistry registry;
|
|
98
|
+
EXPECT(registry.empty());
|
|
99
|
+
EXPECT_EQ((long)registry.tags().size(), 0L);
|
|
100
|
+
|
|
101
|
+
registry.setPoliteness(10, LiveRegionPoliteness::Assertive);
|
|
102
|
+
EXPECT(!registry.empty());
|
|
103
|
+
EXPECT_EQ((long)registry.tags().size(), 1L);
|
|
104
|
+
EXPECT_EQ((long)registry.tags()[0], 10L);
|
|
105
|
+
|
|
106
|
+
registry.setPoliteness(10, LiveRegionPoliteness::None);
|
|
107
|
+
EXPECT(registry.empty());
|
|
108
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// The portable half of a popup menu: how its entries are numbered, which of them
|
|
2
|
+
// are drawn, and where a radio group begins and ends.
|
|
3
|
+
//
|
|
4
|
+
// This is where a popup menu's real bugs have been, and none of them were in the
|
|
5
|
+
// drawing. An index is an entry's position in a pre-order walk of the whole menu,
|
|
6
|
+
// and both sides of a JSI boundary have to agree on it: the hosts tag their items
|
|
7
|
+
// with it and `itemAt` in useContextMenu.ts walks the tree the app passed. The
|
|
8
|
+
// first attempt at roles filtered unsupported ones out of the vector, which would
|
|
9
|
+
// have had the two sides numbering different lists, and on Linux every item after
|
|
10
|
+
// an `about` would have run the handler of the one before it.
|
|
11
|
+
//
|
|
12
|
+
// The drawing is not here and is not asserted anywhere: `presentMenu` answers
|
|
13
|
+
// BASALT_TEST_MENU before `showMenu` is reached, so an automated run never builds
|
|
14
|
+
// a native menu at all. See docs/backlog/desktop-capabilities.md.
|
|
15
|
+
|
|
16
|
+
#include "TestHarness.h"
|
|
17
|
+
|
|
18
|
+
#include "PlatformServices.h"
|
|
19
|
+
#include "TestDialog.h"
|
|
20
|
+
|
|
21
|
+
#include <sstream>
|
|
22
|
+
#include <string>
|
|
23
|
+
#include <vector>
|
|
24
|
+
|
|
25
|
+
namespace {
|
|
26
|
+
|
|
27
|
+
using basalt::MenuEntry;
|
|
28
|
+
|
|
29
|
+
MenuEntry item(std::string label) {
|
|
30
|
+
MenuEntry entry;
|
|
31
|
+
entry.label = std::move(label);
|
|
32
|
+
return entry;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
MenuEntry radio(std::string label, bool checked) {
|
|
36
|
+
MenuEntry entry = item(std::move(label));
|
|
37
|
+
entry.mark = MenuEntry::Mark::Radio;
|
|
38
|
+
entry.checked = checked;
|
|
39
|
+
return entry;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
MenuEntry check(std::string label, bool checked) {
|
|
43
|
+
MenuEntry entry = item(std::move(label));
|
|
44
|
+
entry.mark = MenuEntry::Mark::Check;
|
|
45
|
+
entry.checked = checked;
|
|
46
|
+
return entry;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
MenuEntry parent(std::string label, std::vector<MenuEntry> children) {
|
|
50
|
+
MenuEntry entry = item(std::move(label));
|
|
51
|
+
entry.submenu = std::move(children);
|
|
52
|
+
return entry;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
} // namespace
|
|
56
|
+
|
|
57
|
+
TEST(menu_indexes_are_preorder_and_count_separators_and_parents) {
|
|
58
|
+
// A flat list is numbered by position, which is what an index meant before a
|
|
59
|
+
// popup could nest, so no caller's meaning changed when nesting arrived.
|
|
60
|
+
const std::vector<MenuEntry> flat = {item("a"), MenuEntry::separator(), item("b")};
|
|
61
|
+
EXPECT_EQ(basalt::menuEntryCount(flat), 3);
|
|
62
|
+
EXPECT_EQ(basalt::menuEntryAt(flat, 0)->label, std::string("a"));
|
|
63
|
+
EXPECT(basalt::menuEntryAt(flat, 1)->isSeparator());
|
|
64
|
+
EXPECT_EQ(basalt::menuEntryAt(flat, 2)->label, std::string("b"));
|
|
65
|
+
|
|
66
|
+
// Nested: the parent, then its children, then what follows it.
|
|
67
|
+
const std::vector<MenuEntry> nested = {
|
|
68
|
+
item("first"),
|
|
69
|
+
parent("branch", {item("leaf one"), item("leaf two")}),
|
|
70
|
+
item("last"),
|
|
71
|
+
};
|
|
72
|
+
EXPECT_EQ(basalt::menuEntryCount(nested), 5);
|
|
73
|
+
EXPECT_EQ(basalt::menuEntryAt(nested, 0)->label, std::string("first"));
|
|
74
|
+
EXPECT_EQ(basalt::menuEntryAt(nested, 1)->label, std::string("branch"));
|
|
75
|
+
EXPECT_EQ(basalt::menuEntryAt(nested, 2)->label, std::string("leaf one"));
|
|
76
|
+
EXPECT_EQ(basalt::menuEntryAt(nested, 3)->label, std::string("leaf two"));
|
|
77
|
+
// The one that matters: a parent's children do not push what follows it off by
|
|
78
|
+
// their own count, they are counted where they are.
|
|
79
|
+
EXPECT_EQ(basalt::menuEntryAt(nested, 4)->label, std::string("last"));
|
|
80
|
+
EXPECT(basalt::menuEntryAt(nested, 5) == nullptr);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
TEST(menu_entries_the_platform_drops_still_count) {
|
|
84
|
+
// A role this desktop cannot perform is not drawn. It keeps its index anyway,
|
|
85
|
+
// because the index is into the list JavaScript passed: numbering only what was
|
|
86
|
+
// drawn would move every entry after it onto the wrong handler.
|
|
87
|
+
MenuEntry unsupported = item("about");
|
|
88
|
+
unsupported.role = "not-a-role-any-platform-has";
|
|
89
|
+
const std::vector<MenuEntry> entries = {item("before"), unsupported, item("after")};
|
|
90
|
+
|
|
91
|
+
EXPECT(!basalt::menuEntryShown(entries[1]));
|
|
92
|
+
EXPECT(basalt::menuEntryShown(entries[0]));
|
|
93
|
+
EXPECT_EQ(basalt::menuEntryCount(entries), 3);
|
|
94
|
+
EXPECT_EQ(basalt::menuEntryAt(entries, 2)->label, std::string("after"));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
TEST(a_parent_with_nothing_drawable_under_it_is_not_drawn) {
|
|
98
|
+
MenuEntry unsupported = item("about");
|
|
99
|
+
unsupported.role = "not-a-role-any-platform-has";
|
|
100
|
+
const MenuEntry empty = parent("branch", {unsupported});
|
|
101
|
+
EXPECT(!basalt::menuEntryShown(empty));
|
|
102
|
+
|
|
103
|
+
// One drawable child is enough to keep the parent.
|
|
104
|
+
MenuEntry another = item("about");
|
|
105
|
+
another.role = "not-a-role-any-platform-has";
|
|
106
|
+
const MenuEntry mixed = parent("branch", {another, item("real")});
|
|
107
|
+
EXPECT(basalt::menuEntryShown(mixed));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
TEST(a_radio_run_is_the_adjacent_radio_siblings) {
|
|
111
|
+
// Ended by a non-radio entry, by a separator, and by the end of the level.
|
|
112
|
+
const std::vector<MenuEntry> entries = {
|
|
113
|
+
item("plain"), // 0
|
|
114
|
+
radio("one", true), // 1
|
|
115
|
+
radio("two", false), // 2
|
|
116
|
+
MenuEntry::separator(), // 3
|
|
117
|
+
radio("three", false), // 4
|
|
118
|
+
radio("four", false), // 5
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// The first run is 1..3, stopping at the separator rather than running on.
|
|
122
|
+
EXPECT_EQ(basalt::menuRadioRun(entries, 0, 1).first, 1);
|
|
123
|
+
EXPECT_EQ(basalt::menuRadioRun(entries, 0, 1).second, 3);
|
|
124
|
+
// Asking about any member gives the same run, which is what lets every member
|
|
125
|
+
// of a group share one action named after where the run starts.
|
|
126
|
+
EXPECT_EQ(basalt::menuRadioRun(entries, 0, 2).first, 1);
|
|
127
|
+
EXPECT_EQ(basalt::menuRadioRun(entries, 0, 2).second, 3);
|
|
128
|
+
// The second run is 4..6, ended by the end of the level.
|
|
129
|
+
EXPECT_EQ(basalt::menuRadioRun(entries, 0, 4).first, 4);
|
|
130
|
+
EXPECT_EQ(basalt::menuRadioRun(entries, 0, 4).second, 6);
|
|
131
|
+
// A non-radio entry is its own degenerate run, so a caller need not special-case.
|
|
132
|
+
EXPECT_EQ(basalt::menuRadioRun(entries, 0, 0).first, 0);
|
|
133
|
+
EXPECT_EQ(basalt::menuRadioRun(entries, 0, 0).second, 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
TEST(a_radio_run_is_offset_by_the_level_it_is_on) {
|
|
137
|
+
// Inside a submenu the indexes are the pre-order ones, not positions in the
|
|
138
|
+
// sibling vector, which is what `firstIndexOfLevel` carries. A run named after
|
|
139
|
+
// a sibling position would collide between levels.
|
|
140
|
+
const std::vector<MenuEntry> children = {radio("a", false), radio("b", true)};
|
|
141
|
+
EXPECT_EQ(basalt::menuRadioRun(children, 7, 0).first, 7);
|
|
142
|
+
EXPECT_EQ(basalt::menuRadioRun(children, 7, 0).second, 9);
|
|
143
|
+
EXPECT_EQ(basalt::menuRadioRun(children, 7, 1).first, 7);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
TEST(a_mark_does_not_change_whether_an_entry_is_drawn_or_counted) {
|
|
147
|
+
// A checked item is an ordinary choosable leaf. All three desktops can draw a
|
|
148
|
+
// mark, so nothing here is dropped for carrying one, and the index contract is
|
|
149
|
+
// untouched by it.
|
|
150
|
+
const std::vector<MenuEntry> entries = {check("ticked", true), radio("chosen", true),
|
|
151
|
+
check("unticked", false)};
|
|
152
|
+
for (const MenuEntry &entry : entries) {
|
|
153
|
+
EXPECT(basalt::menuEntryShown(entry));
|
|
154
|
+
EXPECT(!entry.isSeparator());
|
|
155
|
+
EXPECT(!entry.isParent());
|
|
156
|
+
}
|
|
157
|
+
EXPECT_EQ(basalt::menuEntryCount(entries), 3);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- What a script may answer ------------------------------------------------
|
|
161
|
+
//
|
|
162
|
+
// `BASALT_TEST_MENU` skips the presentation and nothing else, so the index it
|
|
163
|
+
// names has to be one a person could have clicked. Four things are not: an
|
|
164
|
+
// index past the end of the tree, a separator, a submenu's parent, and an entry
|
|
165
|
+
// that is drawn greyed. The last was missing until an action sheet put its title
|
|
166
|
+
// into the menu as a disabled entry; see core/ActionSheet.h.
|
|
167
|
+
|
|
168
|
+
TEST(a_script_may_only_answer_what_a_person_could_have_clicked) {
|
|
169
|
+
const std::vector<MenuEntry> entries = {item("first"), MenuEntry::separator(),
|
|
170
|
+
parent("more", {item("nested")}), item("last")};
|
|
171
|
+
|
|
172
|
+
EXPECT_EQ(basalt::scriptedMenuIndex(entries, 0), 0);
|
|
173
|
+
// The separator, which is a line.
|
|
174
|
+
EXPECT_EQ(basalt::scriptedMenuIndex(entries, 1), -1);
|
|
175
|
+
// The parent, which opens rather than chooses -- and its child, which is a
|
|
176
|
+
// real choice at index 3 of the pre-order walk.
|
|
177
|
+
EXPECT_EQ(basalt::scriptedMenuIndex(entries, 2), -1);
|
|
178
|
+
EXPECT_EQ(basalt::scriptedMenuIndex(entries, 3), 3);
|
|
179
|
+
EXPECT_EQ(basalt::scriptedMenuIndex(entries, 4), 4);
|
|
180
|
+
// Past the end, and a dismissal, which is already one.
|
|
181
|
+
EXPECT_EQ(basalt::scriptedMenuIndex(entries, 5), -1);
|
|
182
|
+
EXPECT_EQ(basalt::scriptedMenuIndex(entries, -1), -1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
TEST(a_script_cannot_choose_a_disabled_entry) {
|
|
186
|
+
MenuEntry greyed = item("not now");
|
|
187
|
+
greyed.enabled = false;
|
|
188
|
+
const std::vector<MenuEntry> entries = {greyed, item("go")};
|
|
189
|
+
|
|
190
|
+
EXPECT_EQ(basalt::scriptedMenuIndex(entries, 0), -1);
|
|
191
|
+
EXPECT_EQ(basalt::scriptedMenuIndex(entries, 1), 1);
|
|
192
|
+
}
|
|
@@ -136,3 +136,48 @@ TEST(momentum_a_deceleration_rate_of_one_would_never_stop) {
|
|
|
136
136
|
EXPECT(flight.frames < 6000);
|
|
137
137
|
EXPECT(!momentum.isRunning());
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
// The projection, which is the same model asked where it would end up rather
|
|
141
|
+
// than stepped there.
|
|
142
|
+
//
|
|
143
|
+
// It exists for `core/ScrollSnap.h`: a snapping list has to know where momentum
|
|
144
|
+
// would have carried it before it can choose a point. So the test that matters
|
|
145
|
+
// is that the closed form and the animation agree -- an integral that drifted
|
|
146
|
+
// from the frames would make a list settle somewhere it was never heading.
|
|
147
|
+
TEST(momentum_the_projection_agrees_with_the_frames) {
|
|
148
|
+
ScrollMomentum momentum;
|
|
149
|
+
EXPECT(momentum.start(0, 1200, ScrollMomentum::kNormalDeceleration));
|
|
150
|
+
const Flight flight = flyY(momentum);
|
|
151
|
+
|
|
152
|
+
const double projected =
|
|
153
|
+
basalt::scrollMomentumDistance(1200, ScrollMomentum::kNormalDeceleration);
|
|
154
|
+
EXPECT(projected > 0);
|
|
155
|
+
// Within a few per cent. The stepped version is deliberately the cruder of
|
|
156
|
+
// the two -- it moves at the velocity each frame *started* with, which
|
|
157
|
+
// overshoots slightly -- and ScrollMomentum.cpp says so where it does it.
|
|
158
|
+
EXPECT(projected > flight.distanceY * 0.9);
|
|
159
|
+
EXPECT(projected < flight.distanceY * 1.1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
TEST(momentum_the_projection_keeps_the_sign_and_scales_with_the_throw) {
|
|
163
|
+
const double gentle = basalt::scrollMomentumDistance(400, ScrollMomentum::kNormalDeceleration);
|
|
164
|
+
const double hard = basalt::scrollMomentumDistance(2000, ScrollMomentum::kNormalDeceleration);
|
|
165
|
+
EXPECT(gentle > 0);
|
|
166
|
+
EXPECT(hard > gentle * 2);
|
|
167
|
+
|
|
168
|
+
// Backwards travels backwards, which is the sign convention everything else
|
|
169
|
+
// here uses: positive velocity grows the content offset.
|
|
170
|
+
EXPECT_NEAR(basalt::scrollMomentumDistance(-2000, ScrollMomentum::kNormalDeceleration),
|
|
171
|
+
-hard,
|
|
172
|
+
0.0001);
|
|
173
|
+
|
|
174
|
+
// A faster rate stops sooner, the same way the stepped model does.
|
|
175
|
+
EXPECT(basalt::scrollMomentumDistance(2000, ScrollMomentum::kFastDeceleration) < hard);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
TEST(momentum_a_release_is_projected_nowhere) {
|
|
179
|
+
// The same threshold `start` refuses at: below it there is no fling, so there
|
|
180
|
+
// is nothing to project and a snapping list should settle where it is.
|
|
181
|
+
EXPECT_NEAR(basalt::scrollMomentumDistance(10, ScrollMomentum::kNormalDeceleration), 0.0, 0.0001);
|
|
182
|
+
EXPECT_NEAR(basalt::scrollMomentumDistance(0, ScrollMomentum::kNormalDeceleration), 0.0, 0.0001);
|
|
183
|
+
}
|
|
@@ -64,3 +64,45 @@ TEST(bounds_both_insets_extend_both_ends) {
|
|
|
64
64
|
EXPECT_NEAR(range.maximum, 330.0, 0.001);
|
|
65
65
|
EXPECT_NEAR(range.length(), 350.0, 0.001);
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
// --- centerContent -----------------------------------------------------------
|
|
69
|
+
//
|
|
70
|
+
// An inset rather than a position, because the range above is already the thing
|
|
71
|
+
// that decides where content rests: half the slack at each end leaves exactly
|
|
72
|
+
// one offset to be at, and the clamp every host runs on every mutation puts the
|
|
73
|
+
// content there. iOS reaches it the same way, assigning a `contentInset`.
|
|
74
|
+
|
|
75
|
+
TEST(bounds_centering_insets_are_half_the_slack_at_each_end) {
|
|
76
|
+
const basalt::ScrollAxisInsets insets = basalt::centeringInsets(300, 200);
|
|
77
|
+
EXPECT_NEAR(insets.leading, 50.0, 0.0001);
|
|
78
|
+
EXPECT_NEAR(insets.trailing, 50.0, 0.0001);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
TEST(bounds_centering_leaves_one_offset_to_rest_at) {
|
|
82
|
+
// The whole point: a range of no length, in the middle. So a view that was at
|
|
83
|
+
// zero is pulled to -50, which draws the content half the slack further down.
|
|
84
|
+
const basalt::ScrollRange range =
|
|
85
|
+
basalt::scrollRangeFor(300, 200, basalt::centeringInsets(300, 200));
|
|
86
|
+
EXPECT_NEAR(range.minimum, -50.0, 0.0001);
|
|
87
|
+
EXPECT_NEAR(range.maximum, -50.0, 0.0001);
|
|
88
|
+
EXPECT_NEAR(basalt::clampScrollOffset(0, 300, 200, basalt::centeringInsets(300, 200)),
|
|
89
|
+
-50.0,
|
|
90
|
+
0.0001);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
TEST(bounds_centering_does_nothing_to_content_that_can_scroll) {
|
|
94
|
+
// Content longer than its container has no slack to share out, and an inset
|
|
95
|
+
// here would be a list that could be pulled past its own first row.
|
|
96
|
+
const basalt::ScrollAxisInsets insets = basalt::centeringInsets(300, 4000);
|
|
97
|
+
EXPECT_NEAR(insets.leading, 0.0, 0.0001);
|
|
98
|
+
EXPECT_NEAR(insets.trailing, 0.0, 0.0001);
|
|
99
|
+
EXPECT_NEAR(basalt::clampScrollOffset(0, 300, 4000, insets), 0.0, 0.0001);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
TEST(bounds_centering_content_exactly_the_container_is_not_centred) {
|
|
103
|
+
// The boundary, and the one that would divide by nothing useful: there is no
|
|
104
|
+
// slack, so there is no inset and the view rests at zero.
|
|
105
|
+
const basalt::ScrollAxisInsets insets = basalt::centeringInsets(300, 300);
|
|
106
|
+
EXPECT_NEAR(insets.leading, 0.0, 0.0001);
|
|
107
|
+
EXPECT_NEAR(insets.trailing, 0.0, 0.0001);
|
|
108
|
+
}
|
|
@@ -147,3 +147,64 @@ TEST(indicator_content_that_fits_but_can_be_pulled_still_has_a_bar) {
|
|
|
147
147
|
basalt::scrollIndicatorFor(100.0, 100.0, 0.0, basalt::ScrollAxisInsets{50.0, 0.0});
|
|
148
148
|
EXPECT(indicator.visible);
|
|
149
149
|
}
|
|
150
|
+
|
|
151
|
+
// --- indicatorStyle ----------------------------------------------------------
|
|
152
|
+
//
|
|
153
|
+
// The colour the thumb is drawn in, decided here so that the three hosts cannot
|
|
154
|
+
// answer the same prop differently -- each one used to spell its own black.
|
|
155
|
+
|
|
156
|
+
TEST(indicator_white_is_white) {
|
|
157
|
+
const basalt::ScrollIndicatorColour white =
|
|
158
|
+
basalt::scrollIndicatorColourFor(basalt::ScrollIndicatorStyle::White);
|
|
159
|
+
EXPECT_NEAR(white.red, 1.0, 0.0001);
|
|
160
|
+
EXPECT_NEAR(white.green, 1.0, 0.0001);
|
|
161
|
+
EXPECT_NEAR(white.blue, 1.0, 0.0001);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
TEST(indicator_the_default_is_black) {
|
|
165
|
+
const basalt::ScrollIndicatorColour black =
|
|
166
|
+
basalt::scrollIndicatorColourFor(basalt::ScrollIndicatorStyle::Default);
|
|
167
|
+
EXPECT_NEAR(black.red, 0.0, 0.0001);
|
|
168
|
+
EXPECT_NEAR(black.green, 0.0, 0.0001);
|
|
169
|
+
EXPECT_NEAR(black.blue, 0.0, 0.0001);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
TEST(indicator_default_and_black_share_a_fill_and_differ_by_a_border) {
|
|
173
|
+
// Which is exactly how iOS's two differ:
|
|
174
|
+
// `UIScrollViewIndicatorStyleDefault` is "black with a white border" and
|
|
175
|
+
// `.black` is black alone. The fill being the same is half the claim and
|
|
176
|
+
// used to be the whole of it, back when neither drew a border.
|
|
177
|
+
const basalt::ScrollIndicatorColour fallback =
|
|
178
|
+
basalt::scrollIndicatorColourFor(basalt::ScrollIndicatorStyle::Default);
|
|
179
|
+
const basalt::ScrollIndicatorColour black =
|
|
180
|
+
basalt::scrollIndicatorColourFor(basalt::ScrollIndicatorStyle::Black);
|
|
181
|
+
EXPECT_NEAR(fallback.red, black.red, 0.0001);
|
|
182
|
+
EXPECT_NEAR(fallback.alpha, black.alpha, 0.0001);
|
|
183
|
+
|
|
184
|
+
// And the border is the other half: white, and on `default` alone.
|
|
185
|
+
const basalt::ScrollIndicatorColour border =
|
|
186
|
+
basalt::scrollIndicatorBorderColourFor(basalt::ScrollIndicatorStyle::Default);
|
|
187
|
+
EXPECT_NEAR(border.red, 1.0, 0.0001);
|
|
188
|
+
EXPECT_NEAR(border.green, 1.0, 0.0001);
|
|
189
|
+
EXPECT_NEAR(border.blue, 1.0, 0.0001);
|
|
190
|
+
EXPECT(border.alpha > basalt::kScrollIndicatorAlpha);
|
|
191
|
+
|
|
192
|
+
// The two styles an app picks because it knows what is behind the thumb have
|
|
193
|
+
// none: iOS draws neither with an outline, and adding one would be this
|
|
194
|
+
// platform inventing a look.
|
|
195
|
+
EXPECT_NEAR(basalt::scrollIndicatorBorderColourFor(basalt::ScrollIndicatorStyle::Black).alpha,
|
|
196
|
+
0.0, 0.0001);
|
|
197
|
+
EXPECT_NEAR(basalt::scrollIndicatorBorderColourFor(basalt::ScrollIndicatorStyle::White).alpha,
|
|
198
|
+
0.0, 0.0001);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
TEST(indicator_both_colours_are_equally_translucent) {
|
|
202
|
+
// A white thumb more opaque than the black one would read as a different
|
|
203
|
+
// control rather than the same one in another colour.
|
|
204
|
+
EXPECT_NEAR(basalt::scrollIndicatorColourFor(basalt::ScrollIndicatorStyle::White).alpha,
|
|
205
|
+
basalt::kScrollIndicatorAlpha,
|
|
206
|
+
0.0001);
|
|
207
|
+
EXPECT_NEAR(basalt::scrollIndicatorColourFor(basalt::ScrollIndicatorStyle::Black).alpha,
|
|
208
|
+
basalt::kScrollIndicatorAlpha,
|
|
209
|
+
0.0001);
|
|
210
|
+
}
|
|
@@ -101,6 +101,100 @@ TEST(snap_centre_alignment_lines_up_the_middle) {
|
|
|
101
101
|
EXPECT_EQ(snapped(config, 210, 0), 200.0);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// --- disableIntervalMomentum, and the momentum it disables -----------------
|
|
105
|
+
//
|
|
106
|
+
// A spacing is not a page: upstream lets the fling carry across several points
|
|
107
|
+
// and settles at the one it arrives near, which is what `snapToInterval` means
|
|
108
|
+
// on iOS. The prop asks for the paged behaviour instead.
|
|
109
|
+
//
|
|
110
|
+
// The projection is `core/ScrollMomentum.h`'s, so these numbers are the
|
|
111
|
+
// friction model's rather than a target: 2000px/s at the normal rate is about
|
|
112
|
+
// 984 pixels, which puts the next 250-point boundary at 1000.
|
|
113
|
+
|
|
114
|
+
TEST(snap_a_flick_carries_as_far_as_it_was_thrown) {
|
|
115
|
+
ScrollSnapConfig config;
|
|
116
|
+
config.interval = 250;
|
|
117
|
+
EXPECT_EQ(snapped(config, 0, 2000), 1000.0);
|
|
118
|
+
// And a gentler one carries less, which is the rule the header states and
|
|
119
|
+
// the reason the projection is velocity-dependent rather than a page.
|
|
120
|
+
EXPECT_EQ(snapped(config, 0, 400), 250.0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
TEST(snap_momentum_across_an_interval_can_be_disabled) {
|
|
124
|
+
ScrollSnapConfig config;
|
|
125
|
+
config.interval = 250;
|
|
126
|
+
config.disableIntervalMomentum = true;
|
|
127
|
+
// The next point from where the finger left, however hard the throw.
|
|
128
|
+
EXPECT_EQ(snapped(config, 0, 2000), 250.0);
|
|
129
|
+
EXPECT_EQ(snapped(config, 0, 400), 250.0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
TEST(snap_paging_turns_one_page_however_hard_it_is_thrown) {
|
|
133
|
+
// Not a regression test for the projection so much as a statement that it
|
|
134
|
+
// does not apply here: a paged view turns one page per flick on iOS, and a
|
|
135
|
+
// throw worth two and a half pages still turns one.
|
|
136
|
+
ScrollSnapConfig config;
|
|
137
|
+
config.paging = true;
|
|
138
|
+
EXPECT_EQ(snapped(config, 0, 4000), kContainer);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- snapToStart and snapToEnd ---------------------------------------------
|
|
142
|
+
//
|
|
143
|
+
// Both default to true, and what they are the default *of* is that the
|
|
144
|
+
// content's own edges count as snap points beside the listed ones. Which is
|
|
145
|
+
// easy to miss: a list of offsets that does not include 0 could not be scrolled
|
|
146
|
+
// back to the top and held there.
|
|
147
|
+
|
|
148
|
+
TEST(snap_the_content_edges_are_snap_points_by_default) {
|
|
149
|
+
ScrollSnapConfig config;
|
|
150
|
+
config.offsets = {300, 600};
|
|
151
|
+
|
|
152
|
+
// Nearer the top than the first listed point, released: the top.
|
|
153
|
+
EXPECT_EQ(snapped(config, 100, 0), 0.0);
|
|
154
|
+
// And nearer the end than the last listed point: the end, which is the
|
|
155
|
+
// content minus the container.
|
|
156
|
+
EXPECT_EQ(snapped(config, 1500, 0), kContent - kContainer);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
TEST(snap_to_start_false_scrolls_freely_before_the_first_offset) {
|
|
160
|
+
ScrollSnapConfig config;
|
|
161
|
+
config.offsets = {300, 600};
|
|
162
|
+
config.snapToStart = false;
|
|
163
|
+
// No answer at all, which is what lets the list coast: each host treats a
|
|
164
|
+
// target of nothing as "not a snapping gesture" and runs the fling.
|
|
165
|
+
EXPECT(!scrollSnapTarget(config, 100, 0, kContainer, kContent).has_value());
|
|
166
|
+
// The listed points still snap, so this is the gap being freed rather than
|
|
167
|
+
// snapping being switched off.
|
|
168
|
+
EXPECT_EQ(snapped(config, 320, 0), 300.0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
TEST(snap_to_end_false_scrolls_freely_past_the_last_offset) {
|
|
172
|
+
ScrollSnapConfig config;
|
|
173
|
+
config.offsets = {300, 600};
|
|
174
|
+
config.snapToEnd = false;
|
|
175
|
+
EXPECT(!scrollSnapTarget(config, 1500, 0, kContainer, kContent).has_value());
|
|
176
|
+
// The listed points still snap: it is the gap past the last of them that was
|
|
177
|
+
// freed, not snapping itself. A list at 620 is in that gap and coasts, which
|
|
178
|
+
// is what the assertion above says; one at 320 is not.
|
|
179
|
+
EXPECT_EQ(snapped(config, 320, 0), 300.0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
TEST(snap_a_freed_edge_is_still_reached_from_before_it) {
|
|
183
|
+
// The half of upstream's rule that is easy to drop: a list thrown from
|
|
184
|
+
// *inside* the snapping region towards a freed edge settles on the last
|
|
185
|
+
// listed point rather than sailing into the gap. Only a list already in the
|
|
186
|
+
// gap coasts.
|
|
187
|
+
ScrollSnapConfig config;
|
|
188
|
+
config.offsets = {300, 600};
|
|
189
|
+
config.snapToEnd = false;
|
|
190
|
+
EXPECT_EQ(snapped(config, 500, 2000), 600.0);
|
|
191
|
+
|
|
192
|
+
ScrollSnapConfig fromTheOtherEnd;
|
|
193
|
+
fromTheOtherEnd.offsets = {300, 600};
|
|
194
|
+
fromTheOtherEnd.snapToStart = false;
|
|
195
|
+
EXPECT_EQ(snapped(fromTheOtherEnd, 500, -2000), 300.0);
|
|
196
|
+
}
|
|
197
|
+
|
|
104
198
|
TEST(snap_content_shorter_than_the_container_stays_at_zero) {
|
|
105
199
|
ScrollSnapConfig config;
|
|
106
200
|
config.paging = true;
|
|
@@ -110,3 +204,38 @@ TEST(snap_content_shorter_than_the_container_stays_at_zero) {
|
|
|
110
204
|
}
|
|
111
205
|
|
|
112
206
|
} // namespace
|
|
207
|
+
|
|
208
|
+
// What a tree dump says a scroll view's snapping is, which is the only way an
|
|
209
|
+
// end-to-end run can see any of these props: a snap point changes where a fling
|
|
210
|
+
// *ends*, and a dump is taken when nothing is moving.
|
|
211
|
+
TEST(scroll_snap_describes_itself_for_a_dump) {
|
|
212
|
+
ScrollSnapConfig plain;
|
|
213
|
+
EXPECT(basalt::describeScrollSnap(plain).empty());
|
|
214
|
+
|
|
215
|
+
ScrollSnapConfig paged;
|
|
216
|
+
paged.paging = true;
|
|
217
|
+
EXPECT(basalt::describeScrollSnap(paged) == " snap=paging");
|
|
218
|
+
|
|
219
|
+
ScrollSnapConfig interval;
|
|
220
|
+
interval.interval = 420.0;
|
|
221
|
+
interval.disableIntervalMomentum = true;
|
|
222
|
+
EXPECT(basalt::describeScrollSnap(interval) == " snap=interval:420,momentum=off");
|
|
223
|
+
|
|
224
|
+
ScrollSnapConfig offsets;
|
|
225
|
+
offsets.offsets = {0.0, 100.0, 250.0};
|
|
226
|
+
offsets.alignment = ScrollSnapAlignment::Center;
|
|
227
|
+
offsets.snapToEnd = false;
|
|
228
|
+
EXPECT(basalt::describeScrollSnap(offsets) == " snap=offsets:3,align=center,end=off");
|
|
229
|
+
|
|
230
|
+
// The rate rides along, and only when it is not React Native's `'normal'`:
|
|
231
|
+
// it is what the landing point of a fling is projected with, which is the
|
|
232
|
+
// point a snap is chosen from.
|
|
233
|
+
ScrollSnapConfig fast;
|
|
234
|
+
fast.interval = 100.0;
|
|
235
|
+
fast.decelerationRate = 0.99;
|
|
236
|
+
EXPECT(basalt::describeScrollSnap(fast) == " snap=interval:100 decel=0.99");
|
|
237
|
+
|
|
238
|
+
ScrollSnapConfig rateOnly;
|
|
239
|
+
rateOnly.decelerationRate = 0.9;
|
|
240
|
+
EXPECT(basalt::describeScrollSnap(rateOnly) == " decel=0.9");
|
|
241
|
+
}
|