basalt-win32 0.0.0-stage → 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/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +429 -0
- package/native/tests/test_win32_accessibility.cpp +585 -0
- package/native/tests/test_win32_animated_image.cpp +351 -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 +750 -0
- package/native/tests/test_win32_image.cpp +370 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +511 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +757 -0
- package/native/tests/test_win32_paint.cpp +1799 -0
- package/native/tests/test_win32_scrollview.cpp +647 -0
- package/native/tests/test_win32_selection.cpp +215 -0
- package/native/tests/test_win32_text.cpp +1456 -0
- package/native/tests/test_win32_textinput.cpp +1594 -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 +156 -0
- package/native/win32/DirectWriteLayout.cpp +485 -0
- package/native/win32/DirectWriteLayout.h +77 -0
- package/native/win32/DirectWriteLayoutManager.cpp +154 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +744 -0
- package/native/win32/RnWin32Accessible.cpp +343 -0
- package/native/win32/RnWin32Accessible.h +130 -0
- package/native/win32/RnWin32Image.cpp +588 -0
- package/native/win32/RnWin32Image.h +139 -0
- package/native/win32/RnWin32TextLayout.cpp +1379 -0
- package/native/win32/RnWin32TextLayout.h +390 -0
- package/native/win32/RnWin32View.cpp +2956 -0
- package/native/win32/RnWin32View.h +926 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +209 -0
- package/native/win32/Win32Cursors.cpp +309 -0
- package/native/win32/Win32Cursors.h +121 -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 +199 -0
- package/native/win32/Win32ImageLoader.h +133 -0
- package/native/win32/Win32InputScopes.h +92 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +382 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +1282 -0
- package/native/win32/Win32MountingManager.h +243 -0
- package/native/win32/Win32Offscreen.h +53 -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 +718 -0
- package/native/win32/Win32ScrollView.h +256 -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 +1296 -0
- package/native/win32/Win32TextInput.h +341 -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 +577 -0
- package/native/win32/Win32TouchDispatcher.h +201 -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 +2722 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +25 -4
- package/react-native.config.js +22 -0
- package/README.md +0 -3
|
@@ -0,0 +1,511 @@
|
|
|
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 "Win32Cursors.h"
|
|
22
|
+
#include "Win32MountingManager.h"
|
|
23
|
+
#include "Win32TouchDispatcher.h"
|
|
24
|
+
|
|
25
|
+
#include <react/renderer/components/view/ViewProps.h>
|
|
26
|
+
|
|
27
|
+
#include <cstdint>
|
|
28
|
+
#include <string>
|
|
29
|
+
#include <vector>
|
|
30
|
+
|
|
31
|
+
using basalt::Win32MountingManager;
|
|
32
|
+
using basalt::Win32TouchDispatcher;
|
|
33
|
+
using basalt::win32::RnWin32View;
|
|
34
|
+
using facebook::react::LayoutMetrics;
|
|
35
|
+
using facebook::react::MountingTransaction;
|
|
36
|
+
using facebook::react::ShadowView;
|
|
37
|
+
using facebook::react::ShadowViewMutation;
|
|
38
|
+
using facebook::react::ShadowViewMutationList;
|
|
39
|
+
using facebook::react::SurfaceId;
|
|
40
|
+
using facebook::react::Tag;
|
|
41
|
+
using facebook::react::TransactionTelemetry;
|
|
42
|
+
using facebook::react::ViewProps;
|
|
43
|
+
|
|
44
|
+
namespace {
|
|
45
|
+
|
|
46
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
47
|
+
|
|
48
|
+
ShadowView makeView(Tag tag, float x, float y, float width, float height) {
|
|
49
|
+
LayoutMetrics metrics;
|
|
50
|
+
metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
|
|
51
|
+
|
|
52
|
+
ShadowView view;
|
|
53
|
+
view.componentName = "View";
|
|
54
|
+
view.surfaceId = kSurfaceId;
|
|
55
|
+
view.tag = tag;
|
|
56
|
+
view.props = std::make_shared<ViewProps>();
|
|
57
|
+
view.layoutMetrics = metrics;
|
|
58
|
+
return view;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
void apply(Win32MountingManager &manager, ShadowViewMutationList &&mutations) {
|
|
62
|
+
manager.applyTransaction(
|
|
63
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Creates and inserts one view under `parent` in a single transaction, which is
|
|
67
|
+
// how Fabric would send it.
|
|
68
|
+
void mount(Win32MountingManager &manager,
|
|
69
|
+
Tag parent,
|
|
70
|
+
Tag tag,
|
|
71
|
+
float x,
|
|
72
|
+
float y,
|
|
73
|
+
float width,
|
|
74
|
+
float height) {
|
|
75
|
+
ShadowViewMutationList mutations;
|
|
76
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeView(tag, x, y, width, height)));
|
|
77
|
+
mutations.push_back(
|
|
78
|
+
ShadowViewMutation::InsertMutation(parent, makeView(tag, x, y, width, height), 0));
|
|
79
|
+
apply(manager, std::move(mutations));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The gesture registry is a process-wide singleton -- there is one pointer --
|
|
83
|
+
// so a test that attaches anything has to start by dropping what the last one
|
|
84
|
+
// left behind, and end by dropping its own.
|
|
85
|
+
struct GestureScope {
|
|
86
|
+
std::vector<std::string> states;
|
|
87
|
+
|
|
88
|
+
GestureScope() {
|
|
89
|
+
clear();
|
|
90
|
+
basalt::gestures().setEmitter([this](const std::string &event, folly::dynamic payload) {
|
|
91
|
+
if (event != "onGestureHandlerStateChange") {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
states.push_back(std::to_string(static_cast<int>(payload["state"].asDouble())));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
~GestureScope() {
|
|
99
|
+
basalt::gestures().setEmitter(nullptr);
|
|
100
|
+
clear();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
GestureScope(const GestureScope &) = delete;
|
|
104
|
+
GestureScope &operator=(const GestureScope &) = delete;
|
|
105
|
+
|
|
106
|
+
static void clear() {
|
|
107
|
+
for (int tag = 1; tag <= 32; tag++) {
|
|
108
|
+
basalt::gestures().drop(tag);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// "2,4,5" -- readable in a failure message, which a vector is not.
|
|
113
|
+
std::string joined() const {
|
|
114
|
+
std::string out;
|
|
115
|
+
for (const std::string &state : states) {
|
|
116
|
+
if (!out.empty()) {
|
|
117
|
+
out += ",";
|
|
118
|
+
}
|
|
119
|
+
out += state;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
} // namespace
|
|
126
|
+
|
|
127
|
+
// The dispatcher against a real mounted tree: a tap has to find the tag the
|
|
128
|
+
// mounting manager registered, not just some view.
|
|
129
|
+
TEST(win32_a_synthesised_tap_finds_the_mounted_view) {
|
|
130
|
+
Win32MountingManager manager;
|
|
131
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
132
|
+
root->setFrame(0, 0, 400, 300);
|
|
133
|
+
mount(manager, kSurfaceId, 10, 40, 50, 120, 60);
|
|
134
|
+
|
|
135
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 50, 60)), 10L);
|
|
136
|
+
// Inside the surface but over nothing: the root is the honest answer, because
|
|
137
|
+
// the responder system needs a target for every press inside the surface.
|
|
138
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 5, 5)),
|
|
139
|
+
static_cast<long>(kSurfaceId));
|
|
140
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 5000, 5000)), 0L);
|
|
141
|
+
|
|
142
|
+
// No emitter is attached to these hand-built shadow views, so the tap has
|
|
143
|
+
// nowhere to deliver. It must not crash: that is exactly the shape of a press
|
|
144
|
+
// arriving between a Remove and its Delete.
|
|
145
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
146
|
+
dispatcher.synthesiseTap(50, 60);
|
|
147
|
+
dispatcher.synthesiseTap(5000, 5000);
|
|
148
|
+
|
|
149
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// A move with no button down is hover, and the touch model has no place for it.
|
|
153
|
+
// Reporting it would look to the responder system like a finger dragging across
|
|
154
|
+
// the screen at all times, which cancels every press before it can fire.
|
|
155
|
+
TEST(win32_a_move_before_a_press_is_ignored) {
|
|
156
|
+
Win32MountingManager manager;
|
|
157
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
158
|
+
root->setFrame(0, 0, 400, 300);
|
|
159
|
+
mount(manager, kSurfaceId, 10, 0, 0, 100, 100);
|
|
160
|
+
|
|
161
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
162
|
+
dispatcher.dispatchTouchMove(10, 10);
|
|
163
|
+
EXPECT(!dispatcher.isDown());
|
|
164
|
+
// An end with no start is a no-op too, which is the case the host hits when
|
|
165
|
+
// a WM_LBUTTONUP arrives after the surface was torn down.
|
|
166
|
+
dispatcher.dispatchTouchEnd(10, 10, basalt::PointerButton::Primary);
|
|
167
|
+
dispatcher.dispatchTouchCancel();
|
|
168
|
+
EXPECT(!dispatcher.isDown());
|
|
169
|
+
|
|
170
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The press/release pair, which is the state the host reads to decide whether
|
|
174
|
+
// to hold capture. A press that hits nothing must not arm it: releasing capture
|
|
175
|
+
// that was never taken is how a window ends up ignoring the mouse entirely.
|
|
176
|
+
TEST(win32_a_press_is_only_outstanding_between_down_and_up) {
|
|
177
|
+
Win32MountingManager manager;
|
|
178
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
179
|
+
root->setFrame(0, 0, 400, 300);
|
|
180
|
+
mount(manager, kSurfaceId, 10, 40, 50, 120, 60);
|
|
181
|
+
|
|
182
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
183
|
+
EXPECT(!dispatcher.isDown());
|
|
184
|
+
dispatcher.dispatchTouchStart(50, 60, basalt::PointerButton::Primary);
|
|
185
|
+
EXPECT(dispatcher.isDown());
|
|
186
|
+
dispatcher.dispatchTouchMove(60, 70);
|
|
187
|
+
EXPECT(dispatcher.isDown());
|
|
188
|
+
dispatcher.dispatchTouchEnd(60, 70, basalt::PointerButton::Primary);
|
|
189
|
+
EXPECT(!dispatcher.isDown());
|
|
190
|
+
|
|
191
|
+
// Outside the surface entirely: nothing was hit, so nothing is down.
|
|
192
|
+
dispatcher.dispatchTouchStart(5000, 5000, basalt::PointerButton::Primary);
|
|
193
|
+
EXPECT(!dispatcher.isDown());
|
|
194
|
+
|
|
195
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// A press with no surface root at all -- which is what the host has between
|
|
199
|
+
// creating the window and starting the surface.
|
|
200
|
+
TEST(win32_a_press_with_no_surface_is_a_no_op) {
|
|
201
|
+
Win32TouchDispatcher dispatcher(nullptr, nullptr);
|
|
202
|
+
dispatcher.dispatchTouchStart(10, 10, basalt::PointerButton::Primary);
|
|
203
|
+
dispatcher.dispatchTouchMove(20, 20);
|
|
204
|
+
dispatcher.dispatchTouchEnd(20, 20, basalt::PointerButton::Primary);
|
|
205
|
+
EXPECT(!dispatcher.isDown());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The dispatcher's other half: a pointer offered to react-native-gesture-handler
|
|
209
|
+
// with the views under it. A pan attached to a mounted view has to see the
|
|
210
|
+
// press, the movement and the release, which is BEGAN, ACTIVE, END.
|
|
211
|
+
TEST(win32_a_drag_drives_a_pan_recogniser) {
|
|
212
|
+
GestureScope scope;
|
|
213
|
+
Win32MountingManager manager;
|
|
214
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
215
|
+
root->setFrame(0, 0, 400, 300);
|
|
216
|
+
mount(manager, kSurfaceId, 10, 40, 50, 120, 60);
|
|
217
|
+
|
|
218
|
+
basalt::gestures().create("PanGestureHandler", 1, folly::dynamic::object());
|
|
219
|
+
basalt::gestures().attach(1, 10);
|
|
220
|
+
|
|
221
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
222
|
+
dispatcher.synthesiseDrag(50, 60, 150, 60, 20);
|
|
223
|
+
|
|
224
|
+
EXPECT_EQ(scope.joined(), std::string("2,4,5"));
|
|
225
|
+
|
|
226
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// A handler attached to an *ancestor* of the pressed view still sees the
|
|
230
|
+
// gesture, which is the whole reason the dispatcher builds a chain rather than
|
|
231
|
+
// reporting the one tag a touch is reported against. Getting this wrong means a
|
|
232
|
+
// <GestureDetector> wrapping a screen never fires, because the press always
|
|
233
|
+
// lands on something inside it.
|
|
234
|
+
TEST(win32_a_gesture_on_an_ancestor_sees_the_press) {
|
|
235
|
+
GestureScope scope;
|
|
236
|
+
Win32MountingManager manager;
|
|
237
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
238
|
+
root->setFrame(0, 0, 400, 300);
|
|
239
|
+
mount(manager, kSurfaceId, 10, 40, 50, 200, 200);
|
|
240
|
+
mount(manager, 10, 11, 10, 10, 50, 50);
|
|
241
|
+
|
|
242
|
+
// The press lands on 11; the handler is on its parent.
|
|
243
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 60, 70)), 11L);
|
|
244
|
+
|
|
245
|
+
basalt::gestures().create("PanGestureHandler", 1, folly::dynamic::object());
|
|
246
|
+
basalt::gestures().attach(1, 10);
|
|
247
|
+
|
|
248
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
249
|
+
dispatcher.synthesiseDrag(60, 70, 160, 70, 20);
|
|
250
|
+
|
|
251
|
+
EXPECT_EQ(scope.joined(), std::string("2,4,5"));
|
|
252
|
+
|
|
253
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// The chain carries each view's origin in root coordinates, and a scrolled
|
|
257
|
+
// ancestor moves it. The recogniser is what reads that origin, so a scroll that
|
|
258
|
+
// the chain does not account for puts a gesture's x/y in the wrong place -- and
|
|
259
|
+
// since hit testing already follows the scroll, the two would disagree about
|
|
260
|
+
// the same press.
|
|
261
|
+
TEST(win32_the_hit_chain_follows_a_scroll_offset) {
|
|
262
|
+
GestureScope scope;
|
|
263
|
+
Win32MountingManager manager;
|
|
264
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
265
|
+
root->setFrame(0, 0, 400, 300);
|
|
266
|
+
mount(manager, kSurfaceId, 10, 0, 0, 200, 200);
|
|
267
|
+
mount(manager, 10, 11, 0, 100, 50, 50);
|
|
268
|
+
|
|
269
|
+
// Scrolled down by 100, so the child that was at y=100 is now at the top.
|
|
270
|
+
manager.viewForTag(10)->setScrollOffset(0, 100);
|
|
271
|
+
EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 10, 10)), 11L);
|
|
272
|
+
|
|
273
|
+
basalt::gestures().create("PanGestureHandler", 1, folly::dynamic::object());
|
|
274
|
+
basalt::gestures().attach(1, 11);
|
|
275
|
+
|
|
276
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
277
|
+
dispatcher.synthesiseDrag(10, 10, 110, 10, 20);
|
|
278
|
+
|
|
279
|
+
EXPECT_EQ(scope.joined(), std::string("2,4,5"));
|
|
280
|
+
|
|
281
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// A native handler that activates owns the pointer, and React Native's touch
|
|
285
|
+
// has to be cancelled or a pan over a <Pressable> both pans and presses it.
|
|
286
|
+
// The observable half here is that the dispatcher stops considering a press
|
|
287
|
+
// outstanding the moment the handler activates.
|
|
288
|
+
TEST(win32_an_activated_gesture_takes_the_pointer) {
|
|
289
|
+
GestureScope scope;
|
|
290
|
+
Win32MountingManager manager;
|
|
291
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
292
|
+
root->setFrame(0, 0, 400, 300);
|
|
293
|
+
mount(manager, kSurfaceId, 10, 0, 0, 200, 200);
|
|
294
|
+
|
|
295
|
+
// A native handler activates on contact rather than waiting for movement,
|
|
296
|
+
// which makes the handover observable from a single press.
|
|
297
|
+
basalt::gestures().create("NativeViewGestureHandler", 1, folly::dynamic::object());
|
|
298
|
+
basalt::gestures().attach(1, 10);
|
|
299
|
+
|
|
300
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
301
|
+
dispatcher.dispatchTouchStart(50, 50, basalt::PointerButton::Primary);
|
|
302
|
+
EXPECT(!dispatcher.isDown());
|
|
303
|
+
|
|
304
|
+
dispatcher.dispatchTouchEnd(50, 50, basalt::PointerButton::Primary);
|
|
305
|
+
|
|
306
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Hover runs only for views that asked for it, and the ask arrives as props on
|
|
310
|
+
// every Create and Update. Recorded by the mutation walk, so a cursor crossing
|
|
311
|
+
// an app that uses no hover costs a hit test and nothing else.
|
|
312
|
+
TEST(win32_a_hover_listener_is_remembered_from_the_props) {
|
|
313
|
+
Win32MountingManager manager;
|
|
314
|
+
manager.createSurfaceRoot(kSurfaceId);
|
|
315
|
+
|
|
316
|
+
ShadowView listening = makeView(10, 40, 50, 120, 60);
|
|
317
|
+
auto props = std::make_shared<ViewProps>();
|
|
318
|
+
props->events[facebook::react::ViewEvents::Offset::PointerEnter] = true;
|
|
319
|
+
listening.props = props;
|
|
320
|
+
|
|
321
|
+
ShadowViewMutationList mutations;
|
|
322
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(listening));
|
|
323
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, listening, 0));
|
|
324
|
+
apply(manager, std::move(mutations));
|
|
325
|
+
|
|
326
|
+
EXPECT_EQ(manager.hoverListenersForTag(10),
|
|
327
|
+
static_cast<std::uint16_t>(basalt::HoverListenerEnter));
|
|
328
|
+
// A view that never asked is not stored at all.
|
|
329
|
+
EXPECT_EQ(manager.hoverListenersForTag(kSurfaceId),
|
|
330
|
+
static_cast<std::uint16_t>(basalt::HoverListenerNone));
|
|
331
|
+
|
|
332
|
+
// An update can take the listener away again, and leaving the old mask behind
|
|
333
|
+
// would mean dispatching to a view that stopped listening.
|
|
334
|
+
ShadowViewMutationList updates;
|
|
335
|
+
updates.push_back(ShadowViewMutation::UpdateMutation(
|
|
336
|
+
listening, makeView(10, 40, 50, 120, 60), kSurfaceId));
|
|
337
|
+
apply(manager, std::move(updates));
|
|
338
|
+
EXPECT_EQ(manager.hoverListenersForTag(10),
|
|
339
|
+
static_cast<std::uint16_t>(basalt::HoverListenerNone));
|
|
340
|
+
|
|
341
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// The same shape as the synthesised tap: no emitter is attached to these
|
|
345
|
+
// hand-built shadow views, so every hover has nowhere to deliver and must not
|
|
346
|
+
// crash -- which is also what a cursor moving during a surface teardown looks
|
|
347
|
+
// like from here.
|
|
348
|
+
TEST(win32_a_synthesised_hover_walks_the_tree_without_an_emitter) {
|
|
349
|
+
Win32MountingManager manager;
|
|
350
|
+
RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
|
|
351
|
+
root->setFrame(0, 0, 400, 300);
|
|
352
|
+
mount(manager, kSurfaceId, 10, 40, 50, 120, 60);
|
|
353
|
+
|
|
354
|
+
Win32TouchDispatcher dispatcher(&manager, root);
|
|
355
|
+
dispatcher.synthesiseHover(50, 60);
|
|
356
|
+
dispatcher.synthesiseHover(5, 5);
|
|
357
|
+
// Outside the surface entirely, and then the cursor leaving the window.
|
|
358
|
+
dispatcher.synthesiseHover(5000, 5000);
|
|
359
|
+
dispatcher.synthesiseHover(-1, -1);
|
|
360
|
+
|
|
361
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// The whole of `cursor`, as a keyword list rather than as a hit test.
|
|
366
|
+
// See win32/Win32Cursors.h.
|
|
367
|
+
//
|
|
368
|
+
// The cursor tests in tests/test_win32_hittest.cpp ask which view is under the
|
|
369
|
+
// pointer and whether the keywords Windows has a stock cursor for land on the
|
|
370
|
+
// right one. This one asks the question those cannot: whether the list React
|
|
371
|
+
// Native accepts is covered at all. It sits beside the touch dispatcher because
|
|
372
|
+
// the list is a React Native fact -- `core/CursorNames.h` prints it from
|
|
373
|
+
// `facebook::react::Cursor` -- and only the shards that have React Native can
|
|
374
|
+
// be held to it.
|
|
375
|
+
//
|
|
376
|
+
// What breaks without this test is already in the tree and passing:
|
|
377
|
+
// `cursor_a_keyword_windows_has_nothing_for_leaves_the_pointer_alone` asserts
|
|
378
|
+
// the gap rather than closing it, so the suite stays green while six keywords
|
|
379
|
+
// do nothing. An app that puts `cursor: 'zoom-in'` on an image gets the arrow
|
|
380
|
+
// it already had, with no error and no warning, and a drag source that asked
|
|
381
|
+
// for `copy` or `alias` to say what a drop will do gets no feedback at all --
|
|
382
|
+
// which for those two keywords is the entire feature, the pointer being the
|
|
383
|
+
// only thing a drag can speak through.
|
|
384
|
+
//
|
|
385
|
+
// So this claims the stronger thing the support page's `partial` is measured
|
|
386
|
+
// against: every keyword React Native accepts resolves to some cursor, no
|
|
387
|
+
// keyword falls back to the default arrow unless it *is* `default`, and two
|
|
388
|
+
// keywords share a cursor only inside the families below, where the host
|
|
389
|
+
// documents the sharing. It fails today on the six the backlog entry names and
|
|
390
|
+
// on nothing else: `alias`, `cell`, `context-menu`, `copy`, `zoom-in` and
|
|
391
|
+
// `zoom-out`, each of which answers no cursor whatever.
|
|
392
|
+
//
|
|
393
|
+
// `none` is deliberately not in the list. It is an absence rather than an
|
|
394
|
+
// image, `SetCursor(nullptr)`, and the `hidden` flag is what says so; the
|
|
395
|
+
// hit-test suite covers it. `auto` and `url` reach a host as no keyword at all.
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
TEST(win32_every_cursor_keyword_resolves_to_a_cursor_of_its_own) {
|
|
399
|
+
// CSS's list, grouped into the families that may legitimately share one
|
|
400
|
+
// cursor. A family is the exception the claim allows; everything else has to
|
|
401
|
+
// differ. The first family is the arrow, and nothing outside it may be.
|
|
402
|
+
const std::vector<std::vector<std::string>> families = {
|
|
403
|
+
{"default"},
|
|
404
|
+
{"pointer"},
|
|
405
|
+
// No vertical I-beam in the stock set, so `vertical-text` wears the
|
|
406
|
+
// horizontal one. A narrower complaint than the six below, since it is a
|
|
407
|
+
// cursor that means roughly the right thing rather than no cursor.
|
|
408
|
+
{"text", "vertical-text"},
|
|
409
|
+
{"crosshair"},
|
|
410
|
+
{"wait"},
|
|
411
|
+
// Busy but still usable, which is the arrow with the hourglass beside it.
|
|
412
|
+
{"progress"},
|
|
413
|
+
{"help"},
|
|
414
|
+
// CSS's own pair: both mean "this drop will not happen".
|
|
415
|
+
{"not-allowed", "no-drop"},
|
|
416
|
+
// Windows draws no difference between taking hold of something and
|
|
417
|
+
// moving it, and the shell uses one cursor for all four.
|
|
418
|
+
{"move", "all-scroll", "grab", "grabbing"},
|
|
419
|
+
// The eight directional resizes collapse onto the four `IDC_SIZE*`
|
|
420
|
+
// arrows the way a window's own edges do.
|
|
421
|
+
{"ew-resize", "col-resize", "e-resize", "w-resize"},
|
|
422
|
+
{"ns-resize", "row-resize", "n-resize", "s-resize"},
|
|
423
|
+
{"nesw-resize", "ne-resize", "sw-resize"},
|
|
424
|
+
{"nwse-resize", "nw-resize", "se-resize"},
|
|
425
|
+
// And the six with nothing. Each is its own family because each means
|
|
426
|
+
// its own thing: a shortcut, a spreadsheet cell, a menu about to open, a
|
|
427
|
+
// copy rather than a move, and magnifying in rather than out.
|
|
428
|
+
{"alias"},
|
|
429
|
+
{"cell"},
|
|
430
|
+
{"context-menu"},
|
|
431
|
+
{"copy"},
|
|
432
|
+
{"zoom-in"},
|
|
433
|
+
{"zoom-out"}};
|
|
434
|
+
|
|
435
|
+
struct Resolved {
|
|
436
|
+
std::string keyword;
|
|
437
|
+
int family;
|
|
438
|
+
basalt::win32::Win32Cursor answer;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
std::vector<Resolved> resolved;
|
|
442
|
+
int family = 0;
|
|
443
|
+
for (const std::vector<std::string> &keywords : families) {
|
|
444
|
+
for (const std::string &keyword : keywords) {
|
|
445
|
+
resolved.push_back(Resolved{keyword, family, basalt::win32::win32CursorFor(keyword)});
|
|
446
|
+
}
|
|
447
|
+
family++;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Every keyword answers a cursor. Collected into one string rather than
|
|
451
|
+
// asserted in the loop so that a failure names all of them at once, the way
|
|
452
|
+
// GestureScope::joined does for states.
|
|
453
|
+
std::string withoutOne;
|
|
454
|
+
for (const Resolved &entry : resolved) {
|
|
455
|
+
// `hidden` as well as null: a keyword answered by hiding the pointer has
|
|
456
|
+
// no cursor of its own either, and hiding the pointer over a cell or a
|
|
457
|
+
// shortcut would be worse than the arrow it replaces.
|
|
458
|
+
if (entry.answer.cursor != nullptr && !entry.answer.hidden) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (!withoutOne.empty()) {
|
|
462
|
+
withoutOne += ",";
|
|
463
|
+
}
|
|
464
|
+
withoutOne += entry.keyword;
|
|
465
|
+
}
|
|
466
|
+
EXPECT_EQ(withoutOne, std::string(""));
|
|
467
|
+
|
|
468
|
+
// No two keywords from different families wear the same cursor. Null answers
|
|
469
|
+
// are skipped: every null equals every other null, and the check above has
|
|
470
|
+
// already named them, so this reports only genuine collisions.
|
|
471
|
+
std::string collided;
|
|
472
|
+
for (const Resolved &left : resolved) {
|
|
473
|
+
for (const Resolved &right : resolved) {
|
|
474
|
+
// Each cross-family pair once, which is also how same-family pairs are
|
|
475
|
+
// left alone.
|
|
476
|
+
if (left.family >= right.family) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (left.answer.cursor == nullptr || right.answer.cursor == nullptr) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (left.answer.cursor != right.answer.cursor) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (!collided.empty()) {
|
|
486
|
+
collided += ",";
|
|
487
|
+
}
|
|
488
|
+
collided += left.keyword + "=" + right.keyword;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
EXPECT_EQ(collided, std::string(""));
|
|
492
|
+
|
|
493
|
+
// And the failure this is really about, named directly rather than left to
|
|
494
|
+
// the loop above: the arrow belongs to `default`. A keyword mapped onto
|
|
495
|
+
// `IDC_ARROW` to stop it answering nothing is the same silence with a handle
|
|
496
|
+
// in it, because the arrow is what the window class already shows.
|
|
497
|
+
const HCURSOR arrow = LoadCursorW(nullptr, IDC_ARROW);
|
|
498
|
+
EXPECT(basalt::win32::win32CursorFor("default").cursor == arrow);
|
|
499
|
+
|
|
500
|
+
std::string wearingTheArrow;
|
|
501
|
+
for (const Resolved &entry : resolved) {
|
|
502
|
+
if (entry.keyword == "default" || entry.answer.cursor != arrow) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (!wearingTheArrow.empty()) {
|
|
506
|
+
wearingTheArrow += ",";
|
|
507
|
+
}
|
|
508
|
+
wearingTheArrow += entry.keyword;
|
|
509
|
+
}
|
|
510
|
+
EXPECT_EQ(wearingTheArrow, std::string(""));
|
|
511
|
+
}
|
|
@@ -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
|
+
}
|