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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +429 -0
  5. package/native/tests/test_win32_accessibility.cpp +585 -0
  6. package/native/tests/test_win32_animated_image.cpp +351 -0
  7. package/native/tests/test_win32_clipboard.cpp +121 -0
  8. package/native/tests/test_win32_focus.cpp +195 -0
  9. package/native/tests/test_win32_hittest.cpp +750 -0
  10. package/native/tests/test_win32_image.cpp +370 -0
  11. package/native/tests/test_win32_imageloader.cpp +208 -0
  12. package/native/tests/test_win32_input.cpp +511 -0
  13. package/native/tests/test_win32_key_events.cpp +84 -0
  14. package/native/tests/test_win32_mounting.cpp +757 -0
  15. package/native/tests/test_win32_paint.cpp +1799 -0
  16. package/native/tests/test_win32_scrollview.cpp +647 -0
  17. package/native/tests/test_win32_selection.cpp +215 -0
  18. package/native/tests/test_win32_text.cpp +1456 -0
  19. package/native/tests/test_win32_textinput.cpp +1594 -0
  20. package/native/tests/test_win32_titlebar.cpp +111 -0
  21. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  22. package/native/tests/test_win32_view.cpp +222 -0
  23. package/native/win32/ColorSchemeWin32.cpp +56 -0
  24. package/native/win32/ComponentRegistryWin32.cpp +156 -0
  25. package/native/win32/DirectWriteLayout.cpp +485 -0
  26. package/native/win32/DirectWriteLayout.h +77 -0
  27. package/native/win32/DirectWriteLayoutManager.cpp +154 -0
  28. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  29. package/native/win32/PlatformServicesWin32.cpp +744 -0
  30. package/native/win32/RnWin32Accessible.cpp +343 -0
  31. package/native/win32/RnWin32Accessible.h +130 -0
  32. package/native/win32/RnWin32Image.cpp +588 -0
  33. package/native/win32/RnWin32Image.h +139 -0
  34. package/native/win32/RnWin32TextLayout.cpp +1379 -0
  35. package/native/win32/RnWin32TextLayout.h +390 -0
  36. package/native/win32/RnWin32View.cpp +2956 -0
  37. package/native/win32/RnWin32View.h +926 -0
  38. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  39. package/native/win32/Win32AnimationChoreographer.h +61 -0
  40. package/native/win32/Win32Clip.h +209 -0
  41. package/native/win32/Win32Cursors.cpp +309 -0
  42. package/native/win32/Win32Cursors.h +121 -0
  43. package/native/win32/Win32DropTarget.cpp +567 -0
  44. package/native/win32/Win32DropTarget.h +42 -0
  45. package/native/win32/Win32Focus.cpp +262 -0
  46. package/native/win32/Win32Focus.h +109 -0
  47. package/native/win32/Win32ImageLoader.cpp +199 -0
  48. package/native/win32/Win32ImageLoader.h +133 -0
  49. package/native/win32/Win32InputScopes.h +92 -0
  50. package/native/win32/Win32KeyEvents.cpp +126 -0
  51. package/native/win32/Win32KeyEvents.h +20 -0
  52. package/native/win32/Win32MenuBar.cpp +382 -0
  53. package/native/win32/Win32MenuBar.h +16 -0
  54. package/native/win32/Win32MountingManager.cpp +1282 -0
  55. package/native/win32/Win32MountingManager.h +243 -0
  56. package/native/win32/Win32Offscreen.h +53 -0
  57. package/native/win32/Win32Packaging.cpp +161 -0
  58. package/native/win32/Win32Packaging.h +56 -0
  59. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  60. package/native/win32/Win32RunLoopObserver.h +42 -0
  61. package/native/win32/Win32ScrollView.cpp +718 -0
  62. package/native/win32/Win32ScrollView.h +256 -0
  63. package/native/win32/Win32SkiaContext.cpp +221 -0
  64. package/native/win32/Win32SkiaContext.h +53 -0
  65. package/native/win32/Win32SkiaModule.cpp +80 -0
  66. package/native/win32/Win32SkiaModule.h +50 -0
  67. package/native/win32/Win32SkiaPeer.cpp +358 -0
  68. package/native/win32/Win32SkiaPeer.h +66 -0
  69. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  70. package/native/win32/Win32Snapshot.cpp +201 -0
  71. package/native/win32/Win32Snapshot.h +66 -0
  72. package/native/win32/Win32Strings.h +54 -0
  73. package/native/win32/Win32TextInput.cpp +1296 -0
  74. package/native/win32/Win32TextInput.h +341 -0
  75. package/native/win32/Win32TitleBar.cpp +562 -0
  76. package/native/win32/Win32TitleBar.h +146 -0
  77. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  78. package/native/win32/Win32TitleBarLayout.h +58 -0
  79. package/native/win32/Win32TouchDispatcher.cpp +577 -0
  80. package/native/win32/Win32TouchDispatcher.h +201 -0
  81. package/native/win32/Win32UiThread.h +27 -0
  82. package/native/win32/Win32WindowControl.cpp +329 -0
  83. package/native/win32/Win32WindowModule.cpp +209 -0
  84. package/native/win32/Win32WindowModule.h +72 -0
  85. package/native/win32/demo_layout_win32.cpp +232 -0
  86. package/native/win32/main_win32.cpp +2722 -0
  87. package/native/win32/mount_harness_win32.cpp +316 -0
  88. package/package.json +25 -4
  89. package/react-native.config.js +22 -0
  90. 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
+ }