basalt-core 0.0.0-rc5
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/LICENSE +21 -0
- package/dist/cli/basalt.d.ts +3 -0
- package/dist/cli/basalt.d.ts.map +1 -0
- package/dist/cli/basalt.js +47 -0
- package/dist/cli/basalt.js.map +1 -0
- package/dist/cli/bundle-cli.d.ts +12 -0
- package/dist/cli/bundle-cli.d.ts.map +1 -0
- package/dist/cli/bundle-cli.js +124 -0
- package/dist/cli/bundle-cli.js.map +1 -0
- package/dist/cli/bundleWithAssets.d.ts +79 -0
- package/dist/cli/bundleWithAssets.d.ts.map +1 -0
- package/dist/cli/bundleWithAssets.js +198 -0
- package/dist/cli/bundleWithAssets.js.map +1 -0
- package/dist/cli/desktop.d.ts +339 -0
- package/dist/cli/desktop.d.ts.map +1 -0
- package/dist/cli/desktop.js +1029 -0
- package/dist/cli/desktop.js.map +1 -0
- package/dist/cli/doctor.d.ts +31 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +283 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init.d.ts +77 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +442 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/metro.d.ts +49 -0
- package/dist/cli/metro.d.ts.map +1 -0
- package/dist/cli/metro.js +148 -0
- package/dist/cli/metro.js.map +1 -0
- package/dist/cli/packageApp.d.ts +102 -0
- package/dist/cli/packageApp.d.ts.map +1 -0
- package/dist/cli/packageApp.js +319 -0
- package/dist/cli/packageApp.js.map +1 -0
- package/dist/cli/steps.d.ts +34 -0
- package/dist/cli/steps.d.ts.map +1 -0
- package/dist/cli/steps.js +53 -0
- package/dist/cli/steps.js.map +1 -0
- package/dist/metro-config.d.ts +168 -0
- package/dist/metro-config.d.ts.map +1 -0
- package/dist/metro-config.js +671 -0
- package/dist/metro-config.js.map +1 -0
- package/dist/react-native.config.d.ts +7 -0
- package/dist/react-native.config.d.ts.map +1 -0
- package/dist/react-native.config.js +80 -0
- package/dist/react-native.config.js.map +1 -0
- package/dist/src/DragSource.d.ts +52 -0
- package/dist/src/DragSource.d.ts.map +1 -0
- package/dist/src/DragSource.js +97 -0
- package/dist/src/DragSource.js.map +1 -0
- package/dist/src/DropTarget.d.ts +58 -0
- package/dist/src/DropTarget.d.ts.map +1 -0
- package/dist/src/DropTarget.js +131 -0
- package/dist/src/DropTarget.js.map +1 -0
- package/dist/src/KeyHandler.d.ts +57 -0
- package/dist/src/KeyHandler.d.ts.map +1 -0
- package/dist/src/KeyHandler.js +48 -0
- package/dist/src/KeyHandler.js.map +1 -0
- package/dist/src/Menu.d.ts +72 -0
- package/dist/src/Menu.d.ts.map +1 -0
- package/dist/src/Menu.js +179 -0
- package/dist/src/Menu.js.map +1 -0
- package/dist/src/TitleBar.d.ts +75 -0
- package/dist/src/TitleBar.d.ts.map +1 -0
- package/dist/src/TitleBar.js +236 -0
- package/dist/src/TitleBar.js.map +1 -0
- package/dist/src/Window.d.ts +87 -0
- package/dist/src/Window.d.ts.map +1 -0
- package/dist/src/Window.js +248 -0
- package/dist/src/Window.js.map +1 -0
- package/dist/src/closeRequest.d.ts +55 -0
- package/dist/src/closeRequest.d.ts.map +1 -0
- package/dist/src/closeRequest.js +134 -0
- package/dist/src/closeRequest.js.map +1 -0
- package/dist/src/index.d.ts +45 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +92 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/overrides/Alert.d.ts +69 -0
- package/dist/src/overrides/Alert.d.ts.map +1 -0
- package/dist/src/overrides/Alert.js +112 -0
- package/dist/src/overrides/Alert.js.map +1 -0
- package/dist/src/overrides/BaseViewConfig.d.ts +28 -0
- package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -0
- package/dist/src/overrides/BaseViewConfig.js +72 -0
- package/dist/src/overrides/BaseViewConfig.js.map +1 -0
- package/dist/src/overrides/Platform.linux.d.ts +10 -0
- package/dist/src/overrides/Platform.linux.d.ts.map +1 -0
- package/dist/src/overrides/Platform.linux.js +15 -0
- package/dist/src/overrides/Platform.linux.js.map +1 -0
- package/dist/src/overrides/Platform.macos.d.ts +10 -0
- package/dist/src/overrides/Platform.macos.d.ts.map +1 -0
- package/dist/src/overrides/Platform.macos.js +15 -0
- package/dist/src/overrides/Platform.macos.js.map +1 -0
- package/dist/src/overrides/Platform.windows.d.ts +10 -0
- package/dist/src/overrides/Platform.windows.d.ts.map +1 -0
- package/dist/src/overrides/Platform.windows.js +15 -0
- package/dist/src/overrides/Platform.windows.js.map +1 -0
- package/dist/src/overrides/ReactDevToolsSettingsManager.d.ts +24 -0
- package/dist/src/overrides/ReactDevToolsSettingsManager.d.ts.map +1 -0
- package/dist/src/overrides/ReactDevToolsSettingsManager.js +30 -0
- package/dist/src/overrides/ReactDevToolsSettingsManager.js.map +1 -0
- package/dist/src/overrides/RefreshControl.d.ts +52 -0
- package/dist/src/overrides/RefreshControl.d.ts.map +1 -0
- package/dist/src/overrides/RefreshControl.js +92 -0
- package/dist/src/overrides/RefreshControl.js.map +1 -0
- package/dist/src/overrides/ScrollView.d.ts +39 -0
- package/dist/src/overrides/ScrollView.d.ts.map +1 -0
- package/dist/src/overrides/ScrollView.js +59 -0
- package/dist/src/overrides/ScrollView.js.map +1 -0
- package/dist/src/overrides/Share.d.ts +96 -0
- package/dist/src/overrides/Share.d.ts.map +1 -0
- package/dist/src/overrides/Share.js +97 -0
- package/dist/src/overrides/Share.js.map +1 -0
- package/dist/src/overrides/TextInput.d.ts +81 -0
- package/dist/src/overrides/TextInput.d.ts.map +1 -0
- package/dist/src/overrides/TextInput.js +213 -0
- package/dist/src/overrides/TextInput.js.map +1 -0
- package/dist/src/overrides/View.d.ts +36 -0
- package/dist/src/overrides/View.d.ts.map +1 -0
- package/dist/src/overrides/View.js +99 -0
- package/dist/src/overrides/View.js.map +1 -0
- package/dist/src/overrides/createPlatform.d.ts +32 -0
- package/dist/src/overrides/createPlatform.d.ts.map +1 -0
- package/dist/src/overrides/createPlatform.js +84 -0
- package/dist/src/overrides/createPlatform.js.map +1 -0
- package/dist/src/overrides/setUpDeveloperTools.d.ts +28 -0
- package/dist/src/overrides/setUpDeveloperTools.d.ts.map +1 -0
- package/dist/src/overrides/setUpDeveloperTools.js +84 -0
- package/dist/src/overrides/setUpDeveloperTools.js.map +1 -0
- package/dist/src/overrides/setUpXHR.d.ts +46 -0
- package/dist/src/overrides/setUpXHR.d.ts.map +1 -0
- package/dist/src/overrides/setUpXHR.js +236 -0
- package/dist/src/overrides/setUpXHR.js.map +1 -0
- package/dist/src/quitRequest.d.ts +55 -0
- package/dist/src/quitRequest.d.ts.map +1 -0
- package/dist/src/quitRequest.js +121 -0
- package/dist/src/quitRequest.js.map +1 -0
- package/dist/src/resourcePath.d.ts +18 -0
- package/dist/src/resourcePath.d.ts.map +1 -0
- package/dist/src/resourcePath.js +31 -0
- package/dist/src/resourcePath.js.map +1 -0
- package/dist/src/responseBody.d.ts +76 -0
- package/dist/src/responseBody.d.ts.map +1 -0
- package/dist/src/responseBody.js +94 -0
- package/dist/src/responseBody.js.map +1 -0
- package/dist/src/responseHeaders.d.ts +40 -0
- package/dist/src/responseHeaders.d.ts.map +1 -0
- package/dist/src/responseHeaders.js +75 -0
- package/dist/src/responseHeaders.js.map +1 -0
- package/dist/src/titleBarState.d.ts +43 -0
- package/dist/src/titleBarState.d.ts.map +1 -0
- package/dist/src/titleBarState.js +77 -0
- package/dist/src/titleBarState.js.map +1 -0
- package/dist/src/useContextMenu.d.ts +113 -0
- package/dist/src/useContextMenu.d.ts.map +1 -0
- package/dist/src/useContextMenu.js +109 -0
- package/dist/src/useContextMenu.js.map +1 -0
- package/dist/src/useDialog.d.ts +98 -0
- package/dist/src/useDialog.d.ts.map +1 -0
- package/dist/src/useDialog.js +112 -0
- package/dist/src/useDialog.js.map +1 -0
- package/dist/src/useDisplays.d.ts +70 -0
- package/dist/src/useDisplays.d.ts.map +1 -0
- package/dist/src/useDisplays.js +118 -0
- package/dist/src/useDisplays.js.map +1 -0
- package/dist/src/useHandledKeys.d.ts +38 -0
- package/dist/src/useHandledKeys.d.ts.map +1 -0
- package/dist/src/useHandledKeys.js +70 -0
- package/dist/src/useHandledKeys.js.map +1 -0
- package/dist/src/useWindow.d.ts +140 -0
- package/dist/src/useWindow.d.ts.map +1 -0
- package/dist/src/useWindow.js +233 -0
- package/dist/src/useWindow.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/native/CMakeLists.txt +355 -0
- package/native/bootstrap.sh +613 -0
- package/native/cmake/CompilerFlags.cmake +49 -0
- package/native/cmake/Expo.cmake +125 -0
- package/native/cmake/ReactNativeCore.cmake +612 -0
- package/native/cmake/Reanimated.cmake +108 -0
- package/native/cmake/Skia.cmake +379 -0
- package/native/cmake/ThirdParty.cmake +290 -0
- package/native/cmake/Worklets.cmake +67 -0
- package/native/cmake/codegen/CMakeLists.txt +32 -0
- package/native/core/AppIdentity.cpp +85 -0
- package/native/core/AppIdentity.h +59 -0
- package/native/core/AppPaths.cpp +96 -0
- package/native/core/AppPaths.h +53 -0
- package/native/core/AppearanceModule.cpp +60 -0
- package/native/core/AppearanceModule.h +40 -0
- package/native/core/Backface.h +59 -0
- package/native/core/BlobModule.cpp +283 -0
- package/native/core/BlobModule.h +76 -0
- package/native/core/BlobRegistry.cpp +75 -0
- package/native/core/BlobRegistry.h +44 -0
- package/native/core/ColorScheme.cpp +105 -0
- package/native/core/ColorScheme.h +59 -0
- package/native/core/ComponentRegistry.h +30 -0
- package/native/core/ControlMetrics.h +37 -0
- package/native/core/CoreModules.cpp +592 -0
- package/native/core/CoreModules.h +308 -0
- package/native/core/DebuggingOverlay.cpp +106 -0
- package/native/core/DebuggingOverlay.h +65 -0
- package/native/core/DesktopControls.cpp +232 -0
- package/native/core/DesktopControls.h +170 -0
- package/native/core/DevBundle.cpp +108 -0
- package/native/core/DevBundle.h +69 -0
- package/native/core/DevMenu.cpp +99 -0
- package/native/core/DevMenu.h +75 -0
- package/native/core/DialogModule.cpp +150 -0
- package/native/core/DialogModule.h +51 -0
- package/native/core/DragAndDrop.cpp +45 -0
- package/native/core/DragAndDrop.h +225 -0
- package/native/core/ExpoImageComponent.cpp +79 -0
- package/native/core/ExpoImageComponent.h +80 -0
- package/native/core/ExpoModules.cpp +415 -0
- package/native/core/ExpoModules.h +115 -0
- package/native/core/ExpoRuntime.cpp +366 -0
- package/native/core/ExpoRuntime.h +42 -0
- package/native/core/FocusRing.h +36 -0
- package/native/core/FontRegistry.h +51 -0
- package/native/core/GestureHandlerModule.cpp +136 -0
- package/native/core/GestureHandlerModule.h +71 -0
- package/native/core/Gestures.cpp +533 -0
- package/native/core/Gestures.h +145 -0
- package/native/core/HoverTracker.cpp +69 -0
- package/native/core/HoverTracker.h +109 -0
- package/native/core/HttpClient.cpp +282 -0
- package/native/core/ImageBytes.cpp +205 -0
- package/native/core/ImageBytes.h +29 -0
- package/native/core/ImageCache.cpp +51 -0
- package/native/core/ImageCache.h +89 -0
- package/native/core/JsRuntimeAccess.cpp +70 -0
- package/native/core/JsRuntimeAccess.h +60 -0
- package/native/core/JsiPromise.h +51 -0
- package/native/core/KeyEvents.cpp +116 -0
- package/native/core/KeyEvents.h +144 -0
- package/native/core/LogBoxSurface.cpp +46 -0
- package/native/core/LogBoxSurface.h +68 -0
- package/native/core/MenuModel.h +117 -0
- package/native/core/MenuModule.cpp +209 -0
- package/native/core/MenuModule.h +66 -0
- package/native/core/MountingWalk.h +524 -0
- package/native/core/PackageModules.cpp.in +25 -0
- package/native/core/PackageModules.h +50 -0
- package/native/core/PlatformConstantsModule.cpp +63 -0
- package/native/core/PlatformConstantsModule.h +64 -0
- package/native/core/PlatformServices.h +213 -0
- package/native/core/PointerButtons.h +63 -0
- package/native/core/PullToRefresh.cpp +57 -0
- package/native/core/PullToRefresh.h +73 -0
- package/native/core/ReanimatedCompat.h +47 -0
- package/native/core/ReanimatedModule.cpp +203 -0
- package/native/core/ReanimatedModule.h +55 -0
- package/native/core/ScrollAnimation.cpp +59 -0
- package/native/core/ScrollAnimation.h +70 -0
- package/native/core/ScrollBounds.cpp +26 -0
- package/native/core/ScrollBounds.h +68 -0
- package/native/core/ScrollIndicator.cpp +50 -0
- package/native/core/ScrollIndicator.h +69 -0
- package/native/core/ScrollMomentum.cpp +71 -0
- package/native/core/ScrollMomentum.h +73 -0
- package/native/core/ScrollSnap.cpp +123 -0
- package/native/core/ScrollSnap.h +80 -0
- package/native/core/ShareFallback.cpp +101 -0
- package/native/core/ShareFallback.h +45 -0
- package/native/core/SkiaPictureViewComponent.cpp +11 -0
- package/native/core/SkiaPictureViewComponent.h +47 -0
- package/native/core/SourceCodeModule.cpp +55 -0
- package/native/core/SourceCodeModule.h +53 -0
- package/native/core/StatusBarModule.cpp +23 -0
- package/native/core/StatusBarModule.h +42 -0
- package/native/core/TestDialog.cpp +148 -0
- package/native/core/TestDialog.h +96 -0
- package/native/core/TestKeys.cpp +78 -0
- package/native/core/TestKeys.h +50 -0
- package/native/core/TestQuitFile.cpp +33 -0
- package/native/core/TestQuitFile.h +60 -0
- package/native/core/TestSettle.h +192 -0
- package/native/core/TitleBarRegions.h +31 -0
- package/native/core/TurboModuleProxy.cpp +51 -0
- package/native/core/TurboModuleProxy.h +46 -0
- package/native/core/UIManagerAccess.cpp +43 -0
- package/native/core/UIManagerAccess.h +43 -0
- package/native/core/WindowBoundsCache.cpp +319 -0
- package/native/core/WindowControl.h +235 -0
- package/native/core/WindowHost.h +158 -0
- package/native/core/WindowMethods.cpp +167 -0
- package/native/core/WindowMethods.h +84 -0
- package/native/core/WindowsModule.cpp +448 -0
- package/native/core/WindowsModule.h +133 -0
- package/native/core/WorkletsModule.cpp +199 -0
- package/native/core/WorkletsModule.h +57 -0
- package/native/core/portability_probe.cpp +210 -0
- package/native/tests/EventRecorder.h +124 -0
- package/native/tests/TestHarness.cpp +109 -0
- package/native/tests/TestHarness.h +89 -0
- package/native/tests/TreeDump.h +49 -0
- package/native/tests/test_app_identity.cpp +114 -0
- package/native/tests/test_app_paths.cpp +48 -0
- package/native/tests/test_appearance.cpp +107 -0
- package/native/tests/test_backface.cpp +79 -0
- package/native/tests/test_blobs.cpp +157 -0
- package/native/tests/test_controls.cpp +185 -0
- package/native/tests/test_devbundle.cpp +103 -0
- package/native/tests/test_drag_and_drop.cpp +143 -0
- package/native/tests/test_gestures.cpp +331 -0
- package/native/tests/test_hover.cpp +92 -0
- package/native/tests/test_image_cache.cpp +109 -0
- package/native/tests/test_js_runtime_access.cpp +137 -0
- package/native/tests/test_key_events.cpp +235 -0
- package/native/tests/test_momentum.cpp +138 -0
- package/native/tests/test_quit_file.cpp +104 -0
- package/native/tests/test_scroll_animation.cpp +120 -0
- package/native/tests/test_scroll_bounds.cpp +66 -0
- package/native/tests/test_scroll_indicator.cpp +149 -0
- package/native/tests/test_scroll_snap.cpp +112 -0
- package/native/tests/test_settle.cpp +115 -0
- package/native/tests/test_share.cpp +75 -0
- package/native/tests/test_sourcecode.cpp +62 -0
- package/native/tests/test_window_close.cpp +119 -0
- package/native/tests/test_window_limits.cpp +123 -0
- package/package.json +43 -0
- package/react-native.config.js +86 -0
- package/supported-versions.json +66 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Which view claims which key. The decision, not the delivery.
|
|
2
|
+
//
|
|
3
|
+
// A host asks `handledBy` in the middle of `keyDown:`, `key-pressed` or
|
|
4
|
+
// `WM_KEYDOWN` and consumes the key or passes it on by the answer. So the two
|
|
5
|
+
// failures that matter are opposite and both silent: claiming a key nobody
|
|
6
|
+
// declared swallows it from the menu and from text fields, and failing to claim
|
|
7
|
+
// one that was declared means a shortcut that quietly never fires.
|
|
8
|
+
|
|
9
|
+
#include "TestHarness.h"
|
|
10
|
+
|
|
11
|
+
#include "KeyEvents.h"
|
|
12
|
+
#include "TestKeys.h"
|
|
13
|
+
|
|
14
|
+
#include <sstream>
|
|
15
|
+
#include <string>
|
|
16
|
+
#include <vector>
|
|
17
|
+
|
|
18
|
+
using basalt::KeyCombination;
|
|
19
|
+
using basalt::KeyModifiers;
|
|
20
|
+
|
|
21
|
+
namespace {
|
|
22
|
+
|
|
23
|
+
KeyCombination press(std::string key, KeyModifiers modifiers = {}) {
|
|
24
|
+
return KeyCombination{std::move(key), modifiers};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
KeyModifiers meta() { return KeyModifiers{.alt = false, .ctrl = false, .meta = true, .shift = false}; }
|
|
28
|
+
KeyModifiers metaShift() {
|
|
29
|
+
return KeyModifiers{.alt = false, .ctrl = false, .meta = true, .shift = true};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Tags are React's and mean nothing here, so they are just distinct numbers.
|
|
33
|
+
constexpr facebook::react::Tag kPane = 11;
|
|
34
|
+
constexpr facebook::react::Tag kWindow = 3;
|
|
35
|
+
constexpr facebook::react::Tag kUnrelated = 99;
|
|
36
|
+
|
|
37
|
+
void reset() {
|
|
38
|
+
for (facebook::react::Tag tag : {kPane, kWindow, kUnrelated}) {
|
|
39
|
+
basalt::clearHandledKeys(tag);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
} // namespace
|
|
44
|
+
|
|
45
|
+
TEST(keys_a_view_that_claimed_nothing_claims_nothing) {
|
|
46
|
+
reset();
|
|
47
|
+
// The common case by a wide margin: no app in this repository's suite declares
|
|
48
|
+
// a key, and every one of them presses some.
|
|
49
|
+
EXPECT(!basalt::handlesKey(kPane, press("a")));
|
|
50
|
+
EXPECT(!basalt::handledBy({kPane, kWindow}, press("a")).has_value());
|
|
51
|
+
EXPECT_EQ((int)basalt::handledKeyViewCount(), 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
TEST(keys_a_claim_matches_on_key_and_modifiers) {
|
|
55
|
+
reset();
|
|
56
|
+
basalt::setHandledKeys(kPane, {press("z", meta())});
|
|
57
|
+
|
|
58
|
+
EXPECT(basalt::handlesKey(kPane, press("z", meta())));
|
|
59
|
+
// Same key, no modifier: a different combination, and an app that bound Cmd+Z
|
|
60
|
+
// did not bind Z.
|
|
61
|
+
EXPECT(!basalt::handlesKey(kPane, press("z")));
|
|
62
|
+
// Same modifiers, different key.
|
|
63
|
+
EXPECT(!basalt::handlesKey(kPane, press("y", meta())));
|
|
64
|
+
// A modifier the claim did not ask for. Shift+Cmd+Z is redo and is its own
|
|
65
|
+
// binding; matching it here would fire undo.
|
|
66
|
+
EXPECT(!basalt::handlesKey(kPane, press("z", metaShift())));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
TEST(keys_case_follows_the_character) {
|
|
70
|
+
reset();
|
|
71
|
+
// As in a browser: Shift+A reports "A" with shift set, and an app may bind
|
|
72
|
+
// either. They are different combinations and neither is wrong.
|
|
73
|
+
basalt::setHandledKeys(kPane, {press("A", KeyModifiers{.shift = true})});
|
|
74
|
+
EXPECT(basalt::handlesKey(kPane, press("A", KeyModifiers{.shift = true})));
|
|
75
|
+
EXPECT(!basalt::handlesKey(kPane, press("a", KeyModifiers{.shift = true})));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
TEST(keys_the_innermost_claim_wins) {
|
|
79
|
+
reset();
|
|
80
|
+
// Both claim it. The path runs from the focused view outwards, so the pane
|
|
81
|
+
// answers -- a shortcut on a pane should beat the same one on the window.
|
|
82
|
+
basalt::setHandledKeys(kPane, {press(basalt::kKeyEscape)});
|
|
83
|
+
basalt::setHandledKeys(kWindow, {press(basalt::kKeyEscape)});
|
|
84
|
+
|
|
85
|
+
const auto handler = basalt::handledBy({kPane, kWindow}, press(basalt::kKeyEscape));
|
|
86
|
+
EXPECT(handler.has_value());
|
|
87
|
+
EXPECT_EQ((int)*handler, (int)kPane);
|
|
88
|
+
|
|
89
|
+
// And with the pane out of the path, the window answers: an ancestor's claim
|
|
90
|
+
// is not shadowed by a descendant that exists, only by one in the path.
|
|
91
|
+
const auto outer = basalt::handledBy({kWindow}, press(basalt::kKeyEscape));
|
|
92
|
+
EXPECT(outer.has_value());
|
|
93
|
+
EXPECT_EQ((int)*outer, (int)kWindow);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
TEST(keys_a_view_outside_the_path_is_not_asked) {
|
|
97
|
+
reset();
|
|
98
|
+
// The focus path is the whole of it. A view elsewhere in the tree claiming a
|
|
99
|
+
// key must not swallow it -- that is how a background pane would steal typing.
|
|
100
|
+
basalt::setHandledKeys(kUnrelated, {press("j")});
|
|
101
|
+
EXPECT(!basalt::handledBy({kPane, kWindow}, press("j")).has_value());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
TEST(keys_a_new_declaration_replaces_the_old) {
|
|
105
|
+
reset();
|
|
106
|
+
basalt::setHandledKeys(kPane, {press("j"), press("k"), press("l")});
|
|
107
|
+
EXPECT(basalt::handlesKey(kPane, press("k")));
|
|
108
|
+
|
|
109
|
+
// A re-render hands over the whole list. Merging would keep "k" alive after
|
|
110
|
+
// the app stopped declaring it, which would look like the shortcut working --
|
|
111
|
+
// the failure this list being assigned rather than appended exists to prevent.
|
|
112
|
+
basalt::setHandledKeys(kPane, {press("j"), press("l")});
|
|
113
|
+
EXPECT(basalt::handlesKey(kPane, press("j")));
|
|
114
|
+
EXPECT(!basalt::handlesKey(kPane, press("k")));
|
|
115
|
+
EXPECT_EQ((int)basalt::handledKeyViewCount(), 1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
TEST(keys_an_empty_declaration_is_not_the_same_as_unmounting) {
|
|
119
|
+
reset();
|
|
120
|
+
basalt::setHandledKeys(kPane, {press("j")});
|
|
121
|
+
basalt::setHandledKeys(kPane, {});
|
|
122
|
+
// Still registered, claiming nothing: a view whose shortcuts are all disabled
|
|
123
|
+
// exists and may get them back.
|
|
124
|
+
EXPECT_EQ((int)basalt::handledKeyViewCount(), 1);
|
|
125
|
+
EXPECT(!basalt::handlesKey(kPane, press("j")));
|
|
126
|
+
|
|
127
|
+
basalt::clearHandledKeys(kPane);
|
|
128
|
+
EXPECT_EQ((int)basalt::handledKeyViewCount(), 0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
TEST(keys_a_press_reaches_the_listener) {
|
|
132
|
+
reset();
|
|
133
|
+
basalt::setHandledKeys(kPane, {press("m")});
|
|
134
|
+
|
|
135
|
+
int calls = 0;
|
|
136
|
+
facebook::react::Tag seenTag = 0;
|
|
137
|
+
std::string seenKey;
|
|
138
|
+
bool seenShift = false;
|
|
139
|
+
basalt::setKeyListener([&](facebook::react::Tag tag, const KeyCombination &pressed) {
|
|
140
|
+
calls++;
|
|
141
|
+
seenTag = tag;
|
|
142
|
+
seenKey = pressed.key;
|
|
143
|
+
seenShift = pressed.modifiers.shift;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
basalt::reportKey(kPane, press("m", KeyModifiers{.shift = true}));
|
|
147
|
+
EXPECT_EQ(calls, 1);
|
|
148
|
+
EXPECT_EQ((int)seenTag, (int)kPane);
|
|
149
|
+
EXPECT_EQ(seenKey, std::string("m"));
|
|
150
|
+
EXPECT(seenShift);
|
|
151
|
+
|
|
152
|
+
// Reported as given, not re-matched: the host has already decided, and a
|
|
153
|
+
// second opinion here could disagree with the one the key was consumed on.
|
|
154
|
+
basalt::reportKey(kPane, press("not-a-key"));
|
|
155
|
+
EXPECT_EQ(calls, 2);
|
|
156
|
+
|
|
157
|
+
basalt::setKeyListener(nullptr);
|
|
158
|
+
basalt::reportKey(kPane, press("m"));
|
|
159
|
+
EXPECT_EQ(calls, 2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
TEST(keys_the_special_names_are_the_ones_a_browser_uses) {
|
|
163
|
+
// Spelled once, here, because three hosts mapping their own key codes is three
|
|
164
|
+
// chances to write "Arrowleft" -- and the failure is a shortcut that silently
|
|
165
|
+
// never fires, on one platform only. core/TestQuitFile.h has the same note for
|
|
166
|
+
// the same reason.
|
|
167
|
+
EXPECT_EQ(std::string(basalt::kKeyArrowLeft), std::string("ArrowLeft"));
|
|
168
|
+
EXPECT_EQ(std::string(basalt::kKeyArrowRight), std::string("ArrowRight"));
|
|
169
|
+
EXPECT_EQ(std::string(basalt::kKeyArrowUp), std::string("ArrowUp"));
|
|
170
|
+
EXPECT_EQ(std::string(basalt::kKeyArrowDown), std::string("ArrowDown"));
|
|
171
|
+
EXPECT_EQ(std::string(basalt::kKeyEscape), std::string("Escape"));
|
|
172
|
+
EXPECT_EQ(std::string(basalt::kKeyEnter), std::string("Enter"));
|
|
173
|
+
EXPECT_EQ(std::string(basalt::kKeyTab), std::string("Tab"));
|
|
174
|
+
EXPECT_EQ(std::string(basalt::kKeyPageUp), std::string("PageUp"));
|
|
175
|
+
EXPECT_EQ(std::string(basalt::kKeyPageDown), std::string("PageDown"));
|
|
176
|
+
// The one that looks like a mistake and is not: a space is a character.
|
|
177
|
+
EXPECT_EQ(std::string(basalt::kKeySpace), std::string(" "));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
// --- BASALT_TEST_KEY's syntax ----------------------------------------------
|
|
182
|
+
//
|
|
183
|
+
// Parsed in core rather than in three hosts, because three parsers is three
|
|
184
|
+
// chances to disagree about what "z+meta" means -- and to disagree quietly: the
|
|
185
|
+
// wrong answer is a combination that matches nothing, which a scenario reports as
|
|
186
|
+
// a shortcut that did not fire rather than as a bad instrument.
|
|
187
|
+
|
|
188
|
+
TEST(keys_the_instrument_reads_a_bare_key) {
|
|
189
|
+
const KeyCombination pressed = basalt::parseKeyPress("m");
|
|
190
|
+
EXPECT_EQ(pressed.key, std::string("m"));
|
|
191
|
+
EXPECT(pressed.modifiers == KeyModifiers{});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
TEST(keys_the_instrument_reads_modifiers) {
|
|
195
|
+
const KeyCombination undo = basalt::parseKeyPress("z+meta");
|
|
196
|
+
EXPECT_EQ(undo.key, std::string("z"));
|
|
197
|
+
EXPECT(undo.modifiers.meta);
|
|
198
|
+
EXPECT(!undo.modifiers.shift);
|
|
199
|
+
|
|
200
|
+
const KeyCombination redo = basalt::parseKeyPress("z+meta+shift");
|
|
201
|
+
EXPECT(redo.modifiers.meta);
|
|
202
|
+
EXPECT(redo.modifiers.shift);
|
|
203
|
+
// The pair that must differ, or a test for redo would pass on undo.
|
|
204
|
+
EXPECT(!(undo == redo));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
TEST(keys_the_instrument_reads_a_named_key) {
|
|
208
|
+
EXPECT_EQ(basalt::parseKeyPress("ArrowLeft").key, std::string("ArrowLeft"));
|
|
209
|
+
// A space is a key named " ", and writing it in an environment variable means
|
|
210
|
+
// the parser must not trim.
|
|
211
|
+
EXPECT_EQ(basalt::parseKeyPress(" ").key, std::string(" "));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
TEST(keys_the_instrument_reads_a_literal_plus) {
|
|
215
|
+
// "+" is both the separator and a key an editor binds -- zoom in. Spelt by
|
|
216
|
+
// being first, which is the one case where an empty piece means something.
|
|
217
|
+
EXPECT_EQ(basalt::parseKeyPress("+").key, std::string("+"));
|
|
218
|
+
const KeyCombination zoom = basalt::parseKeyPress("++meta");
|
|
219
|
+
EXPECT_EQ(zoom.key, std::string("+"));
|
|
220
|
+
EXPECT(zoom.modifiers.meta);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
TEST(keys_the_instrument_ignores_a_modifier_it_does_not_know) {
|
|
224
|
+
// A typo gives a combination that matches nothing rather than an error, and the
|
|
225
|
+
// scenario then reports a shortcut that did not fire -- the honest symptom.
|
|
226
|
+
const KeyCombination pressed = basalt::parseKeyPress("z+command");
|
|
227
|
+
EXPECT_EQ(pressed.key, std::string("z"));
|
|
228
|
+
EXPECT(pressed.modifiers == KeyModifiers{});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
TEST(keys_the_instrument_is_empty_when_unset) {
|
|
232
|
+
// Every scenario that does not press a key relies on this, which is all but
|
|
233
|
+
// one of them.
|
|
234
|
+
EXPECT(basalt::scriptedKeyPresses().empty());
|
|
235
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Tests for the fling model.
|
|
2
|
+
//
|
|
3
|
+
// Core's, so compiled into every platform's suite -- although only one platform
|
|
4
|
+
// uses it. macOS decelerates a scroll itself and the AppKit host only reports
|
|
5
|
+
// what the system sends; GTK emits one velocity and stops, so the coasting is
|
|
6
|
+
// this project's. See core/ScrollMomentum.h.
|
|
7
|
+
//
|
|
8
|
+
// Testable at all because the model is a pure function of a velocity and a
|
|
9
|
+
// clock: no widget, no frame clock, and no waiting a second for a fling to
|
|
10
|
+
// finish.
|
|
11
|
+
|
|
12
|
+
#include "TestHarness.h"
|
|
13
|
+
|
|
14
|
+
#include "ScrollMomentum.h"
|
|
15
|
+
|
|
16
|
+
#include <sstream>
|
|
17
|
+
|
|
18
|
+
namespace {
|
|
19
|
+
|
|
20
|
+
using basalt::ScrollMomentum;
|
|
21
|
+
|
|
22
|
+
// A frame at sixty a second, which is what a tick callback hands over.
|
|
23
|
+
constexpr double kFrame = 1.0 / 60.0;
|
|
24
|
+
|
|
25
|
+
// How far a fling travels in total, and how many frames it takes. Bounded, so
|
|
26
|
+
// a model that never stops fails the assertion rather than hanging the suite.
|
|
27
|
+
struct Flight {
|
|
28
|
+
double distanceY{0};
|
|
29
|
+
int frames{0};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
Flight flyY(ScrollMomentum &momentum) {
|
|
33
|
+
Flight flight;
|
|
34
|
+
double dx = 0;
|
|
35
|
+
double dy = 0;
|
|
36
|
+
while (momentum.advance(kFrame, dx, dy) && flight.frames < 6000) {
|
|
37
|
+
flight.distanceY += dy;
|
|
38
|
+
flight.frames++;
|
|
39
|
+
}
|
|
40
|
+
flight.distanceY += dy;
|
|
41
|
+
flight.frames++;
|
|
42
|
+
return flight;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
} // namespace
|
|
46
|
+
|
|
47
|
+
TEST(momentum_a_fling_travels_and_stops) {
|
|
48
|
+
ScrollMomentum momentum;
|
|
49
|
+
EXPECT(momentum.start(0, 1200, ScrollMomentum::kNormalDeceleration));
|
|
50
|
+
EXPECT(momentum.isRunning());
|
|
51
|
+
|
|
52
|
+
const Flight flight = flyY(momentum);
|
|
53
|
+
EXPECT(!momentum.isRunning());
|
|
54
|
+
// The numbers are the model's, not a target: 1200px/s at 0.998 per
|
|
55
|
+
// millisecond comes out at about 595 pixels over about 111 frames, which is
|
|
56
|
+
// roughly what the same flick does on a phone. Asserted as a range, so a
|
|
57
|
+
// change in the constants is caught and a change in the integration step is
|
|
58
|
+
// not.
|
|
59
|
+
EXPECT(flight.distanceY > 450);
|
|
60
|
+
EXPECT(flight.distanceY < 750);
|
|
61
|
+
EXPECT(flight.frames > 60);
|
|
62
|
+
EXPECT(flight.frames < 200);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
TEST(momentum_a_faster_deceleration_travels_less) {
|
|
66
|
+
ScrollMomentum normal;
|
|
67
|
+
normal.start(0, 1200, ScrollMomentum::kNormalDeceleration);
|
|
68
|
+
const Flight slow = flyY(normal);
|
|
69
|
+
|
|
70
|
+
ScrollMomentum fast;
|
|
71
|
+
fast.start(0, 1200, ScrollMomentum::kFastDeceleration);
|
|
72
|
+
const Flight quick = flyY(fast);
|
|
73
|
+
|
|
74
|
+
// What React Native's `decelerationRate` prop is for. 'fast' is the smaller
|
|
75
|
+
// number and the shorter fling, which reads backwards until you remember it
|
|
76
|
+
// is the fraction of the velocity that survives each millisecond.
|
|
77
|
+
EXPECT(quick.distanceY < slow.distanceY);
|
|
78
|
+
EXPECT(quick.frames < slow.frames);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
TEST(momentum_a_fling_keeps_its_direction_on_both_axes) {
|
|
82
|
+
ScrollMomentum momentum;
|
|
83
|
+
momentum.start(-800, 1600, ScrollMomentum::kNormalDeceleration);
|
|
84
|
+
double dx = 0;
|
|
85
|
+
double dy = 0;
|
|
86
|
+
momentum.advance(kFrame, dx, dy);
|
|
87
|
+
EXPECT(dx < 0);
|
|
88
|
+
EXPECT(dy > 0);
|
|
89
|
+
// And in proportion: the model decays a velocity, it does not reshape one.
|
|
90
|
+
EXPECT_NEAR(dy / dx, -2.0, 0.001);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
TEST(momentum_a_slow_flick_is_not_a_fling) {
|
|
94
|
+
ScrollMomentum momentum;
|
|
95
|
+
// Below the stopping speed, so it would report a momentum scroll that began
|
|
96
|
+
// and ended with nothing in between -- worse than not reporting one.
|
|
97
|
+
EXPECT(!momentum.start(0, 5, ScrollMomentum::kNormalDeceleration));
|
|
98
|
+
EXPECT(!momentum.isRunning());
|
|
99
|
+
double dx = 0;
|
|
100
|
+
double dy = 0;
|
|
101
|
+
EXPECT(!momentum.advance(kFrame, dx, dy));
|
|
102
|
+
EXPECT_NEAR(dx, 0.0, 0.0001);
|
|
103
|
+
EXPECT_NEAR(dy, 0.0, 0.0001);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
TEST(momentum_stopping_ends_it_at_once) {
|
|
107
|
+
ScrollMomentum momentum;
|
|
108
|
+
momentum.start(0, 2000, ScrollMomentum::kNormalDeceleration);
|
|
109
|
+
momentum.stop();
|
|
110
|
+
EXPECT(!momentum.isRunning());
|
|
111
|
+
double dx = 0;
|
|
112
|
+
double dy = 0;
|
|
113
|
+
EXPECT(!momentum.advance(kFrame, dx, dy));
|
|
114
|
+
EXPECT_NEAR(dy, 0.0, 0.0001);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
TEST(momentum_a_stalled_frame_clock_does_not_teleport_the_list) {
|
|
118
|
+
ScrollMomentum momentum;
|
|
119
|
+
momentum.start(0, 1000, ScrollMomentum::kNormalDeceleration);
|
|
120
|
+
double dx = 0;
|
|
121
|
+
double dy = 0;
|
|
122
|
+
// A whole second between frames -- a window dragged between monitors, or a
|
|
123
|
+
// machine that swapped. Without a clamp this is a thousand pixels in one
|
|
124
|
+
// step, which reads as the list jumping rather than coasting.
|
|
125
|
+
momentum.advance(1.0, dx, dy);
|
|
126
|
+
EXPECT(dy <= 100.0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
TEST(momentum_a_deceleration_rate_of_one_would_never_stop) {
|
|
130
|
+
ScrollMomentum momentum;
|
|
131
|
+
// `decelerationRate` is a float prop an app can set to anything. A rate of 1
|
|
132
|
+
// keeps the whole velocity every millisecond, which is a list that scrolls
|
|
133
|
+
// for ever; the model clamps rather than trusting it.
|
|
134
|
+
momentum.start(0, 1000, 1.0);
|
|
135
|
+
const Flight flight = flyY(momentum);
|
|
136
|
+
EXPECT(flight.frames < 6000);
|
|
137
|
+
EXPECT(!momentum.isRunning());
|
|
138
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The instrument that lets the harness end a run at a moment of its choosing.
|
|
2
|
+
//
|
|
3
|
+
// What is testable here is the decision -- is the variable set, and is the file
|
|
4
|
+
// there yet -- rather than the quitting, which is three different shutdowns and
|
|
5
|
+
// belongs to each host. This runs on all three, which is the point: the
|
|
6
|
+
// instrument exists because the *portable* way to end a host was a signal, and
|
|
7
|
+
// Windows has no SIGTERM.
|
|
8
|
+
|
|
9
|
+
#include "TestHarness.h"
|
|
10
|
+
|
|
11
|
+
#include "TestQuitFile.h"
|
|
12
|
+
|
|
13
|
+
#include <cstdio>
|
|
14
|
+
#include <cstdlib>
|
|
15
|
+
#include <filesystem>
|
|
16
|
+
#include <fstream>
|
|
17
|
+
#include <sstream>
|
|
18
|
+
#include <string>
|
|
19
|
+
|
|
20
|
+
namespace {
|
|
21
|
+
|
|
22
|
+
// A path that does not exist yet, in the place the platform keeps such things.
|
|
23
|
+
std::string scratchPath() {
|
|
24
|
+
std::error_code ignored;
|
|
25
|
+
const std::filesystem::path path =
|
|
26
|
+
std::filesystem::temp_directory_path(ignored) / "basalt-test-quit-file-probe";
|
|
27
|
+
return path.string();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
void setVariable(const std::string &value) {
|
|
31
|
+
#ifdef _WIN32
|
|
32
|
+
_putenv_s(basalt::kTestQuitFileVar, value.c_str());
|
|
33
|
+
#else
|
|
34
|
+
setenv(basalt::kTestQuitFileVar, value.c_str(), 1);
|
|
35
|
+
#endif
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
} // namespace
|
|
39
|
+
|
|
40
|
+
// Set before anything reads it, because the path is cached on first read -- the
|
|
41
|
+
// environment does not change under a running process, and each host asks for
|
|
42
|
+
// this on a timer. One test therefore establishes it for the file, which is why
|
|
43
|
+
// the ordering below is deliberate rather than incidental.
|
|
44
|
+
TEST(quit_file_reads_the_variable) {
|
|
45
|
+
const std::string path = scratchPath();
|
|
46
|
+
std::error_code ignored;
|
|
47
|
+
std::filesystem::remove(path, ignored);
|
|
48
|
+
setVariable(path);
|
|
49
|
+
|
|
50
|
+
EXPECT(basalt::testQuitFilePath().has_value());
|
|
51
|
+
EXPECT_EQ(*basalt::testQuitFilePath(), path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
TEST(quit_file_is_not_there_until_it_is) {
|
|
55
|
+
const std::string path = *basalt::testQuitFilePath();
|
|
56
|
+
std::error_code ignored;
|
|
57
|
+
std::filesystem::remove(path, ignored);
|
|
58
|
+
|
|
59
|
+
// The whole contract: false until the file exists, and true once it does.
|
|
60
|
+
// A host polls this, so the transition is the instrument.
|
|
61
|
+
EXPECT(!basalt::testQuitFileAppeared());
|
|
62
|
+
|
|
63
|
+
{ std::ofstream created(path); }
|
|
64
|
+
EXPECT(basalt::testQuitFileAppeared());
|
|
65
|
+
|
|
66
|
+
// And not cached, unlike the path: a stale true would quit the next run at
|
|
67
|
+
// startup, before it had done anything, and the failure would look like a
|
|
68
|
+
// crash rather than like a leftover file.
|
|
69
|
+
std::filesystem::remove(path, ignored);
|
|
70
|
+
EXPECT(!basalt::testQuitFileAppeared());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
TEST(quit_file_is_content_free) {
|
|
74
|
+
const std::string path = *basalt::testQuitFilePath();
|
|
75
|
+
std::error_code ignored;
|
|
76
|
+
|
|
77
|
+
// Existence is the message. An empty file is the normal case, because the
|
|
78
|
+
// harness has nothing to say beyond "now" -- and if content mattered there
|
|
79
|
+
// would be a race between creating the file and writing it.
|
|
80
|
+
{ std::ofstream created(path); }
|
|
81
|
+
EXPECT(basalt::testQuitFileAppeared());
|
|
82
|
+
|
|
83
|
+
{ std::ofstream created(path); created << "ignored"; }
|
|
84
|
+
EXPECT(basalt::testQuitFileAppeared());
|
|
85
|
+
|
|
86
|
+
// A directory at that path counts too. Nothing creates one, and reporting it
|
|
87
|
+
// as "not yet" would hang the run instead of ending it.
|
|
88
|
+
std::filesystem::remove(path, ignored);
|
|
89
|
+
std::filesystem::create_directory(path, ignored);
|
|
90
|
+
EXPECT(basalt::testQuitFileAppeared());
|
|
91
|
+
std::filesystem::remove(path, ignored);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
TEST(quit_file_poll_and_message_are_shared) {
|
|
95
|
+
// The three hosts read these rather than spelling them out, so that a person
|
|
96
|
+
// reading three transcripts side by side sees the same shutdown in each.
|
|
97
|
+
// BASALT_QUIT_AFTER_MS had to have its line retrofitted onto Windows after a
|
|
98
|
+
// scenario asserted on a string only two hosts printed.
|
|
99
|
+
EXPECT_EQ(std::string(basalt::kTestQuitFileVar), std::string("BASALT_TEST_QUIT_FILE"));
|
|
100
|
+
EXPECT_EQ(std::string(basalt::kTestQuitFileMessage),
|
|
101
|
+
std::string("BASALT_TEST_QUIT_FILE appeared; quitting"));
|
|
102
|
+
// Responsive enough not to show next to process teardown, and not a busy loop.
|
|
103
|
+
EXPECT(basalt::kTestQuitFilePollMs > 0 && basalt::kTestQuitFilePollMs <= 500);
|
|
104
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// An animated scroll, which is arithmetic and so can be tested without a
|
|
2
|
+
// display -- unlike almost everything else about a scroll view on these hosts.
|
|
3
|
+
|
|
4
|
+
#include "TestHarness.h"
|
|
5
|
+
|
|
6
|
+
#include "ScrollAnimation.h"
|
|
7
|
+
|
|
8
|
+
#include <cmath>
|
|
9
|
+
#include <sstream>
|
|
10
|
+
|
|
11
|
+
using basalt::ScrollAnimation;
|
|
12
|
+
|
|
13
|
+
namespace {
|
|
14
|
+
|
|
15
|
+
TEST(scroll_animation_a_target_equal_to_the_start_is_not_an_animation) {
|
|
16
|
+
// So a caller can apply the offset and be done rather than running a curve
|
|
17
|
+
// that goes nowhere -- and, more to the point, never reporting that it
|
|
18
|
+
// finished.
|
|
19
|
+
ScrollAnimation animation;
|
|
20
|
+
EXPECT(!animation.start(10, 20, 10, 20));
|
|
21
|
+
EXPECT(!animation.isRunning());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
TEST(scroll_animation_arrives_exactly_at_its_target) {
|
|
25
|
+
// Evaluated at the end rather than approached. An app that scrolls to a
|
|
26
|
+
// computed offset and reads it back should get its own number, not one a
|
|
27
|
+
// curve came close to.
|
|
28
|
+
ScrollAnimation animation;
|
|
29
|
+
EXPECT(animation.start(0, 0, 100, 250));
|
|
30
|
+
|
|
31
|
+
double x = 0;
|
|
32
|
+
double y = 0;
|
|
33
|
+
while (animation.advance(1.0 / 60.0, x, y)) {
|
|
34
|
+
}
|
|
35
|
+
EXPECT_EQ(x, 100.0);
|
|
36
|
+
EXPECT_EQ(y, 250.0);
|
|
37
|
+
EXPECT(!animation.isRunning());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
TEST(scroll_animation_moves_monotonically_towards_the_target) {
|
|
41
|
+
ScrollAnimation animation;
|
|
42
|
+
animation.start(0, 0, 0, 300);
|
|
43
|
+
|
|
44
|
+
double x = 0;
|
|
45
|
+
double y = 0;
|
|
46
|
+
double previous = -1;
|
|
47
|
+
int steps = 0;
|
|
48
|
+
while (animation.advance(1.0 / 60.0, x, y)) {
|
|
49
|
+
EXPECT(y >= previous);
|
|
50
|
+
previous = y;
|
|
51
|
+
steps++;
|
|
52
|
+
}
|
|
53
|
+
// Something between "instant" and "a second". At 60Hz a 0.3s curve is about
|
|
54
|
+
// eighteen frames, and asserting the shape rather than the number keeps this
|
|
55
|
+
// from failing if the duration is tuned.
|
|
56
|
+
EXPECT(steps > 5);
|
|
57
|
+
EXPECT(steps < 60);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
TEST(scroll_animation_starts_and_ends_slowly) {
|
|
61
|
+
// The property the curve was chosen for: zero derivative at both ends, so two
|
|
62
|
+
// scrolls in a row have no visible seam. Asserted as "the first step is
|
|
63
|
+
// smaller than a middle step", which is what that means in practice.
|
|
64
|
+
ScrollAnimation animation;
|
|
65
|
+
animation.start(0, 0, 0, 1000);
|
|
66
|
+
|
|
67
|
+
double x = 0;
|
|
68
|
+
double y = 0;
|
|
69
|
+
const double step = ScrollAnimation::kDurationSeconds / 10.0;
|
|
70
|
+
|
|
71
|
+
animation.advance(step, x, y);
|
|
72
|
+
const double first = y;
|
|
73
|
+
|
|
74
|
+
double previous = y;
|
|
75
|
+
for (int i = 0; i < 4; i++) {
|
|
76
|
+
animation.advance(step, x, y);
|
|
77
|
+
}
|
|
78
|
+
const double middle = y - previous;
|
|
79
|
+
|
|
80
|
+
EXPECT(first < middle);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
TEST(scroll_animation_a_stopped_animation_reports_nothing_further) {
|
|
84
|
+
ScrollAnimation animation;
|
|
85
|
+
animation.start(0, 0, 0, 500);
|
|
86
|
+
double x = 0;
|
|
87
|
+
double y = 0;
|
|
88
|
+
animation.advance(0.05, x, y);
|
|
89
|
+
animation.stop();
|
|
90
|
+
EXPECT(!animation.isRunning());
|
|
91
|
+
EXPECT(!animation.advance(0.05, x, y));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
TEST(scroll_animation_a_long_frame_does_not_overshoot) {
|
|
95
|
+
// A stalled frame -- a garbage collection, a slow mount -- hands this more
|
|
96
|
+
// time than the whole animation. It has to land on the target rather than
|
|
97
|
+
// past it, because an offset beyond the content is a scroll view showing
|
|
98
|
+
// nothing.
|
|
99
|
+
ScrollAnimation animation;
|
|
100
|
+
animation.start(0, 0, 0, 400);
|
|
101
|
+
double x = 0;
|
|
102
|
+
double y = 0;
|
|
103
|
+
EXPECT(!animation.advance(5.0, x, y));
|
|
104
|
+
EXPECT_EQ(y, 400.0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
TEST(scroll_animation_runs_backwards_as_well) {
|
|
108
|
+
ScrollAnimation animation;
|
|
109
|
+
animation.start(0, 900, 0, 0);
|
|
110
|
+
double x = 0;
|
|
111
|
+
double y = 900;
|
|
112
|
+
double previous = 901;
|
|
113
|
+
while (animation.advance(1.0 / 60.0, x, y)) {
|
|
114
|
+
EXPECT(y <= previous);
|
|
115
|
+
previous = y;
|
|
116
|
+
}
|
|
117
|
+
EXPECT_EQ(y, 0.0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} // namespace
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The scrollable range, and what `contentInset` does to it.
|
|
2
|
+
//
|
|
3
|
+
// The arithmetic is four lines and was duplicated in three hosts, none of which
|
|
4
|
+
// read the insets. These are the cases that tell the old behaviour from the new
|
|
5
|
+
// one -- and the first three are the old behaviour, which must not move.
|
|
6
|
+
|
|
7
|
+
#include "TestHarness.h"
|
|
8
|
+
|
|
9
|
+
#include "ScrollBounds.h"
|
|
10
|
+
|
|
11
|
+
// EXPECT_NEAR builds its message with a stream; the harness's header does not
|
|
12
|
+
// pull one in, so each suite brings its own.
|
|
13
|
+
#include <sstream>
|
|
14
|
+
|
|
15
|
+
using basalt::clampScrollOffset;
|
|
16
|
+
using basalt::ScrollAxisInsets;
|
|
17
|
+
using basalt::scrollRangeFor;
|
|
18
|
+
|
|
19
|
+
TEST(bounds_without_insets_are_what_they_always_were) {
|
|
20
|
+
const auto range = scrollRangeFor(100.0, 400.0);
|
|
21
|
+
EXPECT_NEAR(range.minimum, 0.0, 0.001);
|
|
22
|
+
EXPECT_NEAR(range.maximum, 300.0, 0.001);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
TEST(bounds_content_that_fits_cannot_scroll) {
|
|
26
|
+
const auto range = scrollRangeFor(400.0, 100.0);
|
|
27
|
+
EXPECT_NEAR(range.minimum, 0.0, 0.001);
|
|
28
|
+
EXPECT_NEAR(range.maximum, 0.0, 0.001);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
TEST(bounds_clamp_holds_an_offset_inside) {
|
|
32
|
+
EXPECT_NEAR(clampScrollOffset(-50.0, 100.0, 400.0), 0.0, 0.001);
|
|
33
|
+
EXPECT_NEAR(clampScrollOffset(999.0, 100.0, 400.0), 300.0, 0.001);
|
|
34
|
+
EXPECT_NEAR(clampScrollOffset(120.0, 100.0, 400.0), 120.0, 0.001);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
TEST(bounds_a_leading_inset_allows_pulling_before_the_content) {
|
|
38
|
+
const auto range = scrollRangeFor(100.0, 400.0, ScrollAxisInsets{50.0, 0.0});
|
|
39
|
+
EXPECT_NEAR(range.minimum, -50.0, 0.001);
|
|
40
|
+
// The far end is unchanged: a top inset does not add room at the bottom.
|
|
41
|
+
EXPECT_NEAR(range.maximum, 300.0, 0.001);
|
|
42
|
+
EXPECT_NEAR(clampScrollOffset(-50.0, 100.0, 400.0, ScrollAxisInsets{50.0, 0.0}), -50.0, 0.001);
|
|
43
|
+
EXPECT_NEAR(clampScrollOffset(-80.0, 100.0, 400.0, ScrollAxisInsets{50.0, 0.0}), -50.0, 0.001);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
TEST(bounds_a_trailing_inset_adds_room_at_the_far_end) {
|
|
47
|
+
const auto range = scrollRangeFor(100.0, 400.0, ScrollAxisInsets{0.0, 40.0});
|
|
48
|
+
EXPECT_NEAR(range.minimum, 0.0, 0.001);
|
|
49
|
+
EXPECT_NEAR(range.maximum, 340.0, 0.001);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Content shorter than its container, with a leading inset: there is somewhere
|
|
53
|
+
// to pull to and nowhere to scroll to, and the two ends must not cross.
|
|
54
|
+
TEST(bounds_an_inset_on_content_that_fits_does_not_invert_the_range) {
|
|
55
|
+
const auto range = scrollRangeFor(400.0, 100.0, ScrollAxisInsets{50.0, 0.0});
|
|
56
|
+
EXPECT_NEAR(range.minimum, -50.0, 0.001);
|
|
57
|
+
EXPECT_NEAR(range.maximum, -50.0, 0.001);
|
|
58
|
+
EXPECT_NEAR(clampScrollOffset(10.0, 400.0, 100.0, ScrollAxisInsets{50.0, 0.0}), -50.0, 0.001);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
TEST(bounds_both_insets_extend_both_ends) {
|
|
62
|
+
const auto range = scrollRangeFor(100.0, 400.0, ScrollAxisInsets{20.0, 30.0});
|
|
63
|
+
EXPECT_NEAR(range.minimum, -20.0, 0.001);
|
|
64
|
+
EXPECT_NEAR(range.maximum, 330.0, 0.001);
|
|
65
|
+
EXPECT_NEAR(range.length(), 350.0, 0.001);
|
|
66
|
+
}
|