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.
Files changed (83) 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 +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. 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
+ }