basalt-win32 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/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +515 -0
- package/native/win32/Win32ScrollView.h +208 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +811 -0
- package/native/win32/Win32TextInput.h +232 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// The image loader: the cache, the ways a load can fail, and the two lifetimes
|
|
2
|
+
// that make this a class rather than a function.
|
|
3
|
+
//
|
|
4
|
+
// The equivalent of `tests/test_image.cpp` on GTK, which asks the same three
|
|
5
|
+
// questions of its loader. What is extra here is the loader outliving its owner
|
|
6
|
+
// -- GTK's loader does not have that hazard, because a completion queued on a
|
|
7
|
+
// GLib source is cancelled with the source, and AppKit's has the hazard and no
|
|
8
|
+
// test.
|
|
9
|
+
//
|
|
10
|
+
// These run with no UI thread installed, which is deliberate and is the
|
|
11
|
+
// configuration `load` detects: with nothing to marshal back to it stays on one
|
|
12
|
+
// thread, so a completion has run by the time `load` returns and nothing here
|
|
13
|
+
// has to wait for anything.
|
|
14
|
+
|
|
15
|
+
#include "TestHarness.h"
|
|
16
|
+
|
|
17
|
+
#include "Win32ImageLoader.h"
|
|
18
|
+
#include "Win32Snapshot.h"
|
|
19
|
+
#include "RnWin32View.h"
|
|
20
|
+
|
|
21
|
+
#include <cstdio>
|
|
22
|
+
#include <cstdlib>
|
|
23
|
+
#include <memory>
|
|
24
|
+
#include <sstream>
|
|
25
|
+
#include <string>
|
|
26
|
+
#include <vector>
|
|
27
|
+
|
|
28
|
+
using basalt::win32::RnWin32Image;
|
|
29
|
+
using basalt::win32::RnWin32View;
|
|
30
|
+
using basalt::win32::Win32ImageLoader;
|
|
31
|
+
|
|
32
|
+
namespace {
|
|
33
|
+
|
|
34
|
+
// A real PNG on disk, written by the snapshot encoder rather than checked in.
|
|
35
|
+
// Cheaper than a fixture and it fails if either half of the image path breaks.
|
|
36
|
+
std::string writeTemporaryPng(const char *name) {
|
|
37
|
+
auto source = std::make_unique<RnWin32View>(1);
|
|
38
|
+
source->setFrame(0, 0, 20, 10);
|
|
39
|
+
source->setBackgroundColor(0.0f, 0.5f, 1.0f, 1.0f, true);
|
|
40
|
+
|
|
41
|
+
const char *temp = std::getenv("TEMP");
|
|
42
|
+
std::string path = std::string(temp != nullptr ? temp : ".") + "\\" + name;
|
|
43
|
+
if (!basalt::win32::writeSnapshot(*source, path)) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
return path;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// file:// is what an app's bundled asset arrives as, and what
|
|
50
|
+
// core/ImageBytes.cpp understands without a network.
|
|
51
|
+
std::string fileUri(const std::string &path) {
|
|
52
|
+
std::string uri = "file:///" + path;
|
|
53
|
+
for (char &c : uri) {
|
|
54
|
+
if (c == '\\') {
|
|
55
|
+
c = '/';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return uri;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
} // namespace
|
|
62
|
+
|
|
63
|
+
TEST(image_loader_decodes_a_file_uri) {
|
|
64
|
+
const std::string path = writeTemporaryPng("basalt_loader_one.png");
|
|
65
|
+
EXPECT(!path.empty());
|
|
66
|
+
|
|
67
|
+
Win32ImageLoader loader;
|
|
68
|
+
std::shared_ptr<RnWin32Image> loaded;
|
|
69
|
+
std::string error;
|
|
70
|
+
loader.load(fileUri(path), [&](std::shared_ptr<RnWin32Image> image, const std::string &why) {
|
|
71
|
+
loaded = std::move(image);
|
|
72
|
+
error = why;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
EXPECT(loaded != nullptr);
|
|
76
|
+
EXPECT_EQ(error, std::string{});
|
|
77
|
+
if (loaded != nullptr) {
|
|
78
|
+
EXPECT_EQ(loaded->width(), 20u);
|
|
79
|
+
EXPECT_EQ(loaded->height(), 10u);
|
|
80
|
+
}
|
|
81
|
+
std::remove(path.c_str());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
TEST(image_loader_serves_a_second_request_from_cache) {
|
|
85
|
+
const std::string path = writeTemporaryPng("basalt_loader_two.png");
|
|
86
|
+
EXPECT(!path.empty());
|
|
87
|
+
const std::string uri = fileUri(path);
|
|
88
|
+
|
|
89
|
+
Win32ImageLoader loader;
|
|
90
|
+
std::shared_ptr<RnWin32Image> first;
|
|
91
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
92
|
+
first = std::move(image);
|
|
93
|
+
});
|
|
94
|
+
EXPECT(first != nullptr);
|
|
95
|
+
|
|
96
|
+
// Deleted before the second request, so a loader that went back to the disk
|
|
97
|
+
// would fail. The same object coming back is what says it did not.
|
|
98
|
+
std::remove(path.c_str());
|
|
99
|
+
|
|
100
|
+
std::shared_ptr<RnWin32Image> second;
|
|
101
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
102
|
+
second = std::move(image);
|
|
103
|
+
});
|
|
104
|
+
EXPECT(second != nullptr);
|
|
105
|
+
EXPECT(first == second);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
TEST(image_loader_reports_a_missing_file) {
|
|
109
|
+
Win32ImageLoader loader;
|
|
110
|
+
std::shared_ptr<RnWin32Image> loaded;
|
|
111
|
+
std::string error;
|
|
112
|
+
loader.load("file:///c:/nothing/here/at/all.png",
|
|
113
|
+
[&](std::shared_ptr<RnWin32Image> image, const std::string &why) {
|
|
114
|
+
loaded = std::move(image);
|
|
115
|
+
error = why;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
EXPECT(loaded == nullptr);
|
|
119
|
+
EXPECT(!error.empty());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
TEST(image_loader_rejects_an_empty_uri) {
|
|
123
|
+
Win32ImageLoader loader;
|
|
124
|
+
bool called = false;
|
|
125
|
+
std::shared_ptr<RnWin32Image> loaded;
|
|
126
|
+
loader.load("", [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
127
|
+
called = true;
|
|
128
|
+
loaded = std::move(image);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Answered rather than ignored: a caller that never hears back cannot tell
|
|
132
|
+
// the difference between a slow load and a dropped one.
|
|
133
|
+
EXPECT(called);
|
|
134
|
+
EXPECT(loaded == nullptr);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
TEST(image_loader_reports_bytes_that_are_not_an_image) {
|
|
138
|
+
const char *temp = std::getenv("TEMP");
|
|
139
|
+
const std::string path = std::string(temp != nullptr ? temp : ".") + "\\basalt_loader_junk.png";
|
|
140
|
+
if (FILE *file = std::fopen(path.c_str(), "wb")) {
|
|
141
|
+
std::fputs("not a png at all", file);
|
|
142
|
+
std::fclose(file);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Win32ImageLoader loader;
|
|
146
|
+
std::shared_ptr<RnWin32Image> loaded;
|
|
147
|
+
std::string error;
|
|
148
|
+
loader.load(fileUri(path), [&](std::shared_ptr<RnWin32Image> image, const std::string &why) {
|
|
149
|
+
loaded = std::move(image);
|
|
150
|
+
error = why;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
EXPECT(loaded == nullptr);
|
|
154
|
+
EXPECT(!error.empty());
|
|
155
|
+
std::remove(path.c_str());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
TEST(image_loader_clears_its_cache) {
|
|
159
|
+
const std::string path = writeTemporaryPng("basalt_loader_three.png");
|
|
160
|
+
EXPECT(!path.empty());
|
|
161
|
+
const std::string uri = fileUri(path);
|
|
162
|
+
|
|
163
|
+
Win32ImageLoader loader;
|
|
164
|
+
std::shared_ptr<RnWin32Image> first;
|
|
165
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
166
|
+
first = std::move(image);
|
|
167
|
+
});
|
|
168
|
+
EXPECT(first != nullptr);
|
|
169
|
+
|
|
170
|
+
loader.clearCache();
|
|
171
|
+
|
|
172
|
+
std::shared_ptr<RnWin32Image> second;
|
|
173
|
+
loader.load(uri, [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
174
|
+
second = std::move(image);
|
|
175
|
+
});
|
|
176
|
+
EXPECT(second != nullptr);
|
|
177
|
+
// A different object: the cache really was dropped and the file read again.
|
|
178
|
+
EXPECT(first != second);
|
|
179
|
+
|
|
180
|
+
std::remove(path.c_str());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
TEST(image_loader_state_outlives_the_loader) {
|
|
184
|
+
// The hazard this class exists to avoid. A load's completion holds a shared
|
|
185
|
+
// reference to the loader's state rather than to the loader, so a loader
|
|
186
|
+
// destroyed between the two is not a use-after-free.
|
|
187
|
+
//
|
|
188
|
+
// Synchronous here, so this cannot catch the race directly -- what it does
|
|
189
|
+
// catch is the shape: destroying a loader that has loaded must not disturb
|
|
190
|
+
// an image its caller is still holding, which would be the symptom if the
|
|
191
|
+
// cache owned rather than shared.
|
|
192
|
+
const std::string path = writeTemporaryPng("basalt_loader_four.png");
|
|
193
|
+
EXPECT(!path.empty());
|
|
194
|
+
|
|
195
|
+
std::shared_ptr<RnWin32Image> survivor;
|
|
196
|
+
{
|
|
197
|
+
Win32ImageLoader loader;
|
|
198
|
+
loader.load(fileUri(path), [&](std::shared_ptr<RnWin32Image> image, const std::string &) {
|
|
199
|
+
survivor = std::move(image);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
EXPECT(survivor != nullptr);
|
|
204
|
+
if (survivor != nullptr) {
|
|
205
|
+
EXPECT_EQ(survivor->width(), 20u);
|
|
206
|
+
}
|
|
207
|
+
std::remove(path.c_str());
|
|
208
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Tests for the touch dispatcher: does a mouse message become the right touch,
|
|
2
|
+
// against the right view, and does a gesture recogniser see the same geometry?
|
|
3
|
+
//
|
|
4
|
+
// Hit testing itself is not here -- it is a pure function of the view tree and
|
|
5
|
+
// needs no React Native, so it lives in tests/test_win32_hittest.cpp with the
|
|
6
|
+
// rest of the view layer. What is here is everything above it: the state
|
|
7
|
+
// machine that decides a move is a drag rather than hover, the tag a touch is
|
|
8
|
+
// reported against, and the hit chain a gesture handler is offered.
|
|
9
|
+
//
|
|
10
|
+
// Nothing below synthesises a real mouse event. Doing that on Windows means
|
|
11
|
+
// SendInput, which moves the actual cursor and so cannot run beside anything
|
|
12
|
+
// else on the machine; the same objection CGEvent raises on macOS. So these
|
|
13
|
+
// enter at `dispatchTouch*`, exactly where the window procedure does, and what
|
|
14
|
+
// goes untested is that Windows routes WM_LBUTTONDOWN here at all.
|
|
15
|
+
|
|
16
|
+
#include "TestHarness.h"
|
|
17
|
+
|
|
18
|
+
#include "PointerButtons.h"
|
|
19
|
+
|
|
20
|
+
#include "Gestures.h"
|
|
21
|
+
#include "Win32MountingManager.h"
|
|
22
|
+
#include "Win32TouchDispatcher.h"
|
|
23
|
+
|
|
24
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
25
|
+
|
|
26
|
+
#include <cstdint>
|
|
27
|
+
#include <string>
|
|
28
|
+
#include <vector>
|
|
29
|
+
|
|
30
|
+
using basalt::Win32MountingManager;
|
|
31
|
+
using basalt::Win32TouchDispatcher;
|
|
32
|
+
using basalt::win32::RnWin32View;
|
|
33
|
+
using facebook::react::LayoutMetrics;
|
|
34
|
+
using facebook::react::MountingTransaction;
|
|
35
|
+
using facebook::react::ShadowView;
|
|
36
|
+
using facebook::react::ShadowViewMutation;
|
|
37
|
+
using facebook::react::ShadowViewMutationList;
|
|
38
|
+
using facebook::react::SurfaceId;
|
|
39
|
+
using facebook::react::Tag;
|
|
40
|
+
using facebook::react::TransactionTelemetry;
|
|
41
|
+
using facebook::react::ViewProps;
|
|
42
|
+
|
|
43
|
+
namespace {
|
|
44
|
+
|
|
45
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
46
|
+
|
|
47
|
+
ShadowView makeView(Tag tag, float x, float y, float width, float height) {
|
|
48
|
+
LayoutMetrics metrics;
|
|
49
|
+
metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
|
|
50
|
+
|
|
51
|
+
ShadowView view;
|
|
52
|
+
view.componentName = "View";
|
|
53
|
+
view.surfaceId = kSurfaceId;
|
|
54
|
+
view.tag = tag;
|
|
55
|
+
view.props = std::make_shared<ViewProps>();
|
|
56
|
+
view.layoutMetrics = metrics;
|
|
57
|
+
return view;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
void apply(Win32MountingManager &manager, ShadowViewMutationList &&mutations) {
|
|
61
|
+
manager.applyTransaction(
|
|
62
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Creates and inserts one view under `parent` in a single transaction, which is
|
|
66
|
+
// how Fabric would send it.
|
|
67
|
+
void mount(Win32MountingManager &manager,
|
|
68
|
+
Tag parent,
|
|
69
|
+
Tag tag,
|
|
70
|
+
float x,
|
|
71
|
+
float y,
|
|
72
|
+
float width,
|
|
73
|
+
float height) {
|
|
74
|
+
ShadowViewMutationList mutations;
|
|
75
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeView(tag, x, y, width, height)));
|
|
76
|
+
mutations.push_back(
|
|
77
|
+
ShadowViewMutation::InsertMutation(parent, makeView(tag, x, y, width, height), 0));
|
|
78
|
+
apply(manager, std::move(mutations));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The gesture registry is a process-wide singleton -- there is one pointer --
|
|
82
|
+
// so a test that attaches anything has to start by dropping what the last one
|
|
83
|
+
// left behind, and end by dropping its own.
|
|
84
|
+
struct GestureScope {
|
|
85
|
+
std::vector<std::string> states;
|
|
86
|
+
|
|
87
|
+
GestureScope() {
|
|
88
|
+
clear();
|
|
89
|
+
basalt::gestures().setEmitter([this](const std::string &event, folly::dynamic payload) {
|
|
90
|
+
if (event != "onGestureHandlerStateChange") {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
states.push_back(std::to_string(static_cast<int>(payload["state"].asDouble())));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
~GestureScope() {
|
|
98
|
+
basalt::gestures().setEmitter(nullptr);
|
|
99
|
+
clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
GestureScope(const GestureScope &) = delete;
|
|
103
|
+
GestureScope &operator=(const GestureScope &) = delete;
|
|
104
|
+
|
|
105
|
+
static void clear() {
|
|
106
|
+
for (int tag = 1; tag <= 32; tag++) {
|
|
107
|
+
basalt::gestures().drop(tag);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// "2,4,5" -- readable in a failure message, which a vector is not.
|
|
112
|
+
std::string joined() const {
|
|
113
|
+
std::string out;
|
|
114
|
+
for (const std::string &state : states) {
|
|
115
|
+
if (!out.empty()) {
|
|
116
|
+
out += ",";
|
|
117
|
+
}
|
|
118
|
+
out += state;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
} // namespace
|
|
125
|
+
|
|
126
|
+
// The dispatcher against a real mounted tree: a tap has to find the tag the
|
|
127
|
+
// mounting manager registered, not just some view.
|
|
128
|
+
TEST(win32_a_synthesised_tap_finds_the_mounted_view) {
|
|
129
|
+
Win32MountingManager manager;
|
|
130
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
131
|
+
root->setFrame(0, 0, 400, 300);
|
|
132
|
+
mount(manager, kSurfaceId, 10, 40, 50, 120, 60);
|
|
133
|
+
|
|
134
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 50, 60)), 10L);
|
|
135
|
+
// Inside the surface but over nothing: the root is the honest answer, because
|
|
136
|
+
// the responder system needs a target for every press inside the surface.
|
|
137
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 5, 5)),
|
|
138
|
+
static_cast<long>(kSurfaceId));
|
|
139
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 5000, 5000)), 0L);
|
|
140
|
+
|
|
141
|
+
// No emitter is attached to these hand-built shadow views, so the tap has
|
|
142
|
+
// nowhere to deliver. It must not crash: that is exactly the shape of a press
|
|
143
|
+
// arriving between a Remove and its Delete.
|
|
144
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
145
|
+
dispatcher.synthesiseTap(50, 60);
|
|
146
|
+
dispatcher.synthesiseTap(5000, 5000);
|
|
147
|
+
|
|
148
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// A move with no button down is hover, and the touch model has no place for it.
|
|
152
|
+
// Reporting it would look to the responder system like a finger dragging across
|
|
153
|
+
// the screen at all times, which cancels every press before it can fire.
|
|
154
|
+
TEST(win32_a_move_before_a_press_is_ignored) {
|
|
155
|
+
Win32MountingManager manager;
|
|
156
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
157
|
+
root->setFrame(0, 0, 400, 300);
|
|
158
|
+
mount(manager, kSurfaceId, 10, 0, 0, 100, 100);
|
|
159
|
+
|
|
160
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
161
|
+
dispatcher.dispatchTouchMove(10, 10);
|
|
162
|
+
EXPECT(!dispatcher.isDown());
|
|
163
|
+
// An end with no start is a no-op too, which is the case the host hits when
|
|
164
|
+
// a WM_LBUTTONUP arrives after the surface was torn down.
|
|
165
|
+
dispatcher.dispatchTouchEnd(10, 10, basalt::PointerButton::Primary);
|
|
166
|
+
dispatcher.dispatchTouchCancel();
|
|
167
|
+
EXPECT(!dispatcher.isDown());
|
|
168
|
+
|
|
169
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// The press/release pair, which is the state the host reads to decide whether
|
|
173
|
+
// to hold capture. A press that hits nothing must not arm it: releasing capture
|
|
174
|
+
// that was never taken is how a window ends up ignoring the mouse entirely.
|
|
175
|
+
TEST(win32_a_press_is_only_outstanding_between_down_and_up) {
|
|
176
|
+
Win32MountingManager manager;
|
|
177
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
178
|
+
root->setFrame(0, 0, 400, 300);
|
|
179
|
+
mount(manager, kSurfaceId, 10, 40, 50, 120, 60);
|
|
180
|
+
|
|
181
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
182
|
+
EXPECT(!dispatcher.isDown());
|
|
183
|
+
dispatcher.dispatchTouchStart(50, 60, basalt::PointerButton::Primary);
|
|
184
|
+
EXPECT(dispatcher.isDown());
|
|
185
|
+
dispatcher.dispatchTouchMove(60, 70);
|
|
186
|
+
EXPECT(dispatcher.isDown());
|
|
187
|
+
dispatcher.dispatchTouchEnd(60, 70, basalt::PointerButton::Primary);
|
|
188
|
+
EXPECT(!dispatcher.isDown());
|
|
189
|
+
|
|
190
|
+
// Outside the surface entirely: nothing was hit, so nothing is down.
|
|
191
|
+
dispatcher.dispatchTouchStart(5000, 5000, basalt::PointerButton::Primary);
|
|
192
|
+
EXPECT(!dispatcher.isDown());
|
|
193
|
+
|
|
194
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// A press with no surface root at all -- which is what the host has between
|
|
198
|
+
// creating the window and starting the surface.
|
|
199
|
+
TEST(win32_a_press_with_no_surface_is_a_no_op) {
|
|
200
|
+
Win32TouchDispatcher dispatcher(nullptr, nullptr);
|
|
201
|
+
dispatcher.dispatchTouchStart(10, 10, basalt::PointerButton::Primary);
|
|
202
|
+
dispatcher.dispatchTouchMove(20, 20);
|
|
203
|
+
dispatcher.dispatchTouchEnd(20, 20, basalt::PointerButton::Primary);
|
|
204
|
+
EXPECT(!dispatcher.isDown());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The dispatcher's other half: a pointer offered to react-native-gesture-handler
|
|
208
|
+
// with the views under it. A pan attached to a mounted view has to see the
|
|
209
|
+
// press, the movement and the release, which is BEGAN, ACTIVE, END.
|
|
210
|
+
TEST(win32_a_drag_drives_a_pan_recogniser) {
|
|
211
|
+
GestureScope scope;
|
|
212
|
+
Win32MountingManager manager;
|
|
213
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
214
|
+
root->setFrame(0, 0, 400, 300);
|
|
215
|
+
mount(manager, kSurfaceId, 10, 40, 50, 120, 60);
|
|
216
|
+
|
|
217
|
+
basalt::gestures().create("PanGestureHandler", 1, folly::dynamic::object());
|
|
218
|
+
basalt::gestures().attach(1, 10);
|
|
219
|
+
|
|
220
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
221
|
+
dispatcher.synthesiseDrag(50, 60, 150, 60, 20);
|
|
222
|
+
|
|
223
|
+
EXPECT_EQ(scope.joined(), std::string("2,4,5"));
|
|
224
|
+
|
|
225
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// A handler attached to an *ancestor* of the pressed view still sees the
|
|
229
|
+
// gesture, which is the whole reason the dispatcher builds a chain rather than
|
|
230
|
+
// reporting the one tag a touch is reported against. Getting this wrong means a
|
|
231
|
+
// <GestureDetector> wrapping a screen never fires, because the press always
|
|
232
|
+
// lands on something inside it.
|
|
233
|
+
TEST(win32_a_gesture_on_an_ancestor_sees_the_press) {
|
|
234
|
+
GestureScope scope;
|
|
235
|
+
Win32MountingManager manager;
|
|
236
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
237
|
+
root->setFrame(0, 0, 400, 300);
|
|
238
|
+
mount(manager, kSurfaceId, 10, 40, 50, 200, 200);
|
|
239
|
+
mount(manager, 10, 11, 10, 10, 50, 50);
|
|
240
|
+
|
|
241
|
+
// The press lands on 11; the handler is on its parent.
|
|
242
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 60, 70)), 11L);
|
|
243
|
+
|
|
244
|
+
basalt::gestures().create("PanGestureHandler", 1, folly::dynamic::object());
|
|
245
|
+
basalt::gestures().attach(1, 10);
|
|
246
|
+
|
|
247
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
248
|
+
dispatcher.synthesiseDrag(60, 70, 160, 70, 20);
|
|
249
|
+
|
|
250
|
+
EXPECT_EQ(scope.joined(), std::string("2,4,5"));
|
|
251
|
+
|
|
252
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The chain carries each view's origin in root coordinates, and a scrolled
|
|
256
|
+
// ancestor moves it. The recogniser is what reads that origin, so a scroll that
|
|
257
|
+
// the chain does not account for puts a gesture's x/y in the wrong place -- and
|
|
258
|
+
// since hit testing already follows the scroll, the two would disagree about
|
|
259
|
+
// the same press.
|
|
260
|
+
TEST(win32_the_hit_chain_follows_a_scroll_offset) {
|
|
261
|
+
GestureScope scope;
|
|
262
|
+
Win32MountingManager manager;
|
|
263
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
264
|
+
root->setFrame(0, 0, 400, 300);
|
|
265
|
+
mount(manager, kSurfaceId, 10, 0, 0, 200, 200);
|
|
266
|
+
mount(manager, 10, 11, 0, 100, 50, 50);
|
|
267
|
+
|
|
268
|
+
// Scrolled down by 100, so the child that was at y=100 is now at the top.
|
|
269
|
+
manager.viewForTag(10)->setScrollOffset(0, 100);
|
|
270
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 10, 10)), 11L);
|
|
271
|
+
|
|
272
|
+
basalt::gestures().create("PanGestureHandler", 1, folly::dynamic::object());
|
|
273
|
+
basalt::gestures().attach(1, 11);
|
|
274
|
+
|
|
275
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
276
|
+
dispatcher.synthesiseDrag(10, 10, 110, 10, 20);
|
|
277
|
+
|
|
278
|
+
EXPECT_EQ(scope.joined(), std::string("2,4,5"));
|
|
279
|
+
|
|
280
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// A native handler that activates owns the pointer, and React Native's touch
|
|
284
|
+
// has to be cancelled or a pan over a <Pressable> both pans and presses it.
|
|
285
|
+
// The observable half here is that the dispatcher stops considering a press
|
|
286
|
+
// outstanding the moment the handler activates.
|
|
287
|
+
TEST(win32_an_activated_gesture_takes_the_pointer) {
|
|
288
|
+
GestureScope scope;
|
|
289
|
+
Win32MountingManager manager;
|
|
290
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
291
|
+
root->setFrame(0, 0, 400, 300);
|
|
292
|
+
mount(manager, kSurfaceId, 10, 0, 0, 200, 200);
|
|
293
|
+
|
|
294
|
+
// A native handler activates on contact rather than waiting for movement,
|
|
295
|
+
// which makes the handover observable from a single press.
|
|
296
|
+
basalt::gestures().create("NativeViewGestureHandler", 1, folly::dynamic::object());
|
|
297
|
+
basalt::gestures().attach(1, 10);
|
|
298
|
+
|
|
299
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
300
|
+
dispatcher.dispatchTouchStart(50, 50, basalt::PointerButton::Primary);
|
|
301
|
+
EXPECT(!dispatcher.isDown());
|
|
302
|
+
|
|
303
|
+
dispatcher.dispatchTouchEnd(50, 50, basalt::PointerButton::Primary);
|
|
304
|
+
|
|
305
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Hover runs only for views that asked for it, and the ask arrives as props on
|
|
309
|
+
// every Create and Update. Recorded by the mutation walk, so a cursor crossing
|
|
310
|
+
// an app that uses no hover costs a hit test and nothing else.
|
|
311
|
+
TEST(win32_a_hover_listener_is_remembered_from_the_props) {
|
|
312
|
+
Win32MountingManager manager;
|
|
313
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
314
|
+
|
|
315
|
+
ShadowView listening = makeView(10, 40, 50, 120, 60);
|
|
316
|
+
auto props = std::make_shared<ViewProps>();
|
|
317
|
+
props->events[facebook::react::ViewEvents::Offset::PointerEnter] = true;
|
|
318
|
+
listening.props = props;
|
|
319
|
+
|
|
320
|
+
ShadowViewMutationList mutations;
|
|
321
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(listening));
|
|
322
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, listening, 0));
|
|
323
|
+
apply(manager, std::move(mutations));
|
|
324
|
+
|
|
325
|
+
EXPECT_EQ(manager.hoverListenersForTag(10),
|
|
326
|
+
static_cast<std::uint16_t>(basalt::HoverListenerEnter));
|
|
327
|
+
// A view that never asked is not stored at all.
|
|
328
|
+
EXPECT_EQ(manager.hoverListenersForTag(kSurfaceId),
|
|
329
|
+
static_cast<std::uint16_t>(basalt::HoverListenerNone));
|
|
330
|
+
|
|
331
|
+
// An update can take the listener away again, and leaving the old mask behind
|
|
332
|
+
// would mean dispatching to a view that stopped listening.
|
|
333
|
+
ShadowViewMutationList updates;
|
|
334
|
+
updates.push_back(ShadowViewMutation::UpdateMutation(
|
|
335
|
+
listening, makeView(10, 40, 50, 120, 60), kSurfaceId));
|
|
336
|
+
apply(manager, std::move(updates));
|
|
337
|
+
EXPECT_EQ(manager.hoverListenersForTag(10),
|
|
338
|
+
static_cast<std::uint16_t>(basalt::HoverListenerNone));
|
|
339
|
+
|
|
340
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// The same shape as the synthesised tap: no emitter is attached to these
|
|
344
|
+
// hand-built shadow views, so every hover has nowhere to deliver and must not
|
|
345
|
+
// crash -- which is also what a cursor moving during a surface teardown looks
|
|
346
|
+
// like from here.
|
|
347
|
+
TEST(win32_a_synthesised_hover_walks_the_tree_without_an_emitter) {
|
|
348
|
+
Win32MountingManager manager;
|
|
349
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
350
|
+
root->setFrame(0, 0, 400, 300);
|
|
351
|
+
mount(manager, kSurfaceId, 10, 40, 50, 120, 60);
|
|
352
|
+
|
|
353
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
354
|
+
dispatcher.synthesiseHover(50, 60);
|
|
355
|
+
dispatcher.synthesiseHover(5, 5);
|
|
356
|
+
// Outside the surface entirely, and then the cursor leaving the window.
|
|
357
|
+
dispatcher.synthesiseHover(5000, 5000);
|
|
358
|
+
dispatcher.synthesiseHover(-1, -1);
|
|
359
|
+
|
|
360
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
361
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// A Win32 key press as a key combination.
|
|
2
|
+
//
|
|
3
|
+
// The third of three mapping suites, and the one that most needs to exist: this
|
|
4
|
+
// host is the one nobody can compile on a Mac, so the only thing that checks the
|
|
5
|
+
// table below is CI. The two silent failures are the same as everywhere -- a
|
|
6
|
+
// misspelt name matches nothing and reports nothing, and a bare modifier offered
|
|
7
|
+
// as a combination fires a shortcut on Ctrl alone.
|
|
8
|
+
//
|
|
9
|
+
// What is *not* covered here is the modifiers. They come from `GetKeyState`
|
|
10
|
+
// rather than from the message, so asserting on them would mean holding a key
|
|
11
|
+
// down in a test process. The combination's key half is what this checks; the
|
|
12
|
+
// modifier half is checked on the two hosts where the state arrives with the
|
|
13
|
+
// event, and the three tables are written to agree.
|
|
14
|
+
|
|
15
|
+
#include "TestHarness.h"
|
|
16
|
+
|
|
17
|
+
#include "Win32KeyEvents.h"
|
|
18
|
+
|
|
19
|
+
#include <windows.h>
|
|
20
|
+
|
|
21
|
+
#include <sstream>
|
|
22
|
+
#include <string>
|
|
23
|
+
|
|
24
|
+
namespace {
|
|
25
|
+
|
|
26
|
+
std::string keyOf(unsigned int virtualKey) {
|
|
27
|
+
const auto combination = basalt::keyCombinationFrom(virtualKey);
|
|
28
|
+
return combination.has_value() ? combination->key : std::string("(none)");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
} // namespace
|
|
32
|
+
|
|
33
|
+
TEST(win32_keys_the_arrows_have_browser_names) {
|
|
34
|
+
EXPECT_EQ(keyOf(VK_LEFT), std::string("ArrowLeft"));
|
|
35
|
+
EXPECT_EQ(keyOf(VK_RIGHT), std::string("ArrowRight"));
|
|
36
|
+
EXPECT_EQ(keyOf(VK_UP), std::string("ArrowUp"));
|
|
37
|
+
EXPECT_EQ(keyOf(VK_DOWN), std::string("ArrowDown"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
TEST(win32_keys_the_navigation_block_has_browser_names) {
|
|
41
|
+
// VK_PRIOR and VK_NEXT are Page Up and Page Down. The names are about
|
|
42
|
+
// priority, not paging, and getting them the wrong way round would be a
|
|
43
|
+
// shortcut that scrolls the wrong direction -- plausible enough to survive.
|
|
44
|
+
EXPECT_EQ(keyOf(VK_PRIOR), std::string("PageUp"));
|
|
45
|
+
EXPECT_EQ(keyOf(VK_NEXT), std::string("PageDown"));
|
|
46
|
+
EXPECT_EQ(keyOf(VK_HOME), std::string("Home"));
|
|
47
|
+
EXPECT_EQ(keyOf(VK_END), std::string("End"));
|
|
48
|
+
EXPECT_EQ(keyOf(VK_DELETE), std::string("Delete"));
|
|
49
|
+
EXPECT_EQ(keyOf(VK_BACK), std::string("Backspace"));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
TEST(win32_keys_escape_enter_tab_and_space) {
|
|
53
|
+
EXPECT_EQ(keyOf(VK_ESCAPE), std::string("Escape"));
|
|
54
|
+
EXPECT_EQ(keyOf(VK_RETURN), std::string("Enter"));
|
|
55
|
+
EXPECT_EQ(keyOf(VK_TAB), std::string("Tab"));
|
|
56
|
+
// A space is named " ". Unlike the other two hosts, Shift+Tab here is VK_TAB
|
|
57
|
+
// with the modifier held rather than a key of its own, so there is nothing to
|
|
58
|
+
// fold together.
|
|
59
|
+
EXPECT_EQ(keyOf(VK_SPACE), std::string(" "));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
TEST(win32_keys_a_bare_modifier_is_not_a_combination) {
|
|
63
|
+
// On the other two hosts a bare modifier is detectable by producing no
|
|
64
|
+
// character. Here the virtual key has to be recognised, so each one is listed
|
|
65
|
+
// and each one is asserted.
|
|
66
|
+
for (unsigned int key : {VK_SHIFT, VK_LSHIFT, VK_RSHIFT, VK_CONTROL, VK_LCONTROL,
|
|
67
|
+
VK_RCONTROL, VK_MENU, VK_LMENU, VK_RMENU, VK_LWIN, VK_RWIN,
|
|
68
|
+
VK_CAPITAL}) {
|
|
69
|
+
EXPECT_EQ(keyOf(key), std::string("(none)"));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
TEST(win32_keys_a_letter_is_lower_case_when_shift_is_not_held) {
|
|
74
|
+
// MapVirtualKeyW hands back upper case for a letter whatever the shift state,
|
|
75
|
+
// so the case is applied by this host rather than arriving with the event. No
|
|
76
|
+
// key is held in a test process, so this is the unshifted answer.
|
|
77
|
+
EXPECT_EQ(keyOf('A'), std::string("a"));
|
|
78
|
+
EXPECT_EQ(keyOf('Z'), std::string("z"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
TEST(win32_keys_a_digit_is_itself) {
|
|
82
|
+
EXPECT_EQ(keyOf('0'), std::string("0"));
|
|
83
|
+
EXPECT_EQ(keyOf('7'), std::string("7"));
|
|
84
|
+
}
|