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,750 @@
1
+ // Hit testing.
2
+ //
3
+ // The same questions tests/test_hittest.cpp asks of GTK and the hit-test half
4
+ // of tests/test_appkit_input.mm asks of AppKit, in the same order, plus two
5
+ // this platform has to answer for itself.
6
+ //
7
+ // Both other hosts inherit picking from their toolkit -- `gtk_widget_pick` and
8
+ // `-[NSView hitTest:]` -- so the work there is making the toolkit's geometry
9
+ // agree with Fabric's. There is no toolkit geometry here, so this is written
10
+ // out, which is more code and one fewer thing that can silently disagree: the
11
+ // transform a view is drawn with is the transform a press is tested against,
12
+ // because `localToParent` is the only place either of them is composed.
13
+
14
+ #include "TestHarness.h"
15
+
16
+ #include "RnWin32View.h"
17
+ #include "Win32Cursors.h"
18
+ #include "Win32InputScopes.h"
19
+
20
+ #include <memory>
21
+ #include <sstream>
22
+ #include <string>
23
+ #include <vector>
24
+
25
+ using basalt::win32::hitTest;
26
+ using basalt::win32::RnWin32View;
27
+
28
+ namespace {
29
+
30
+ class Tree {
31
+ public:
32
+ RnWin32View *box(int32_t tag, float x, float y, float w, float h) {
33
+ auto view = std::make_unique<RnWin32View>(tag);
34
+ view->setFrame(x, y, w, h);
35
+ RnWin32View *raw = view.get();
36
+ views_.push_back(std::move(view));
37
+ return raw;
38
+ }
39
+
40
+ private:
41
+ std::vector<std::unique_ptr<RnWin32View>> views_;
42
+ };
43
+
44
+ // The tag of whatever is under a point, or -1 for a miss. Comparing tags rather
45
+ // than pointers makes a failure say which view it found.
46
+ int32_t tagAt(RnWin32View *root, float x, float y) {
47
+ RnWin32View *hit = hitTest(root, x, y);
48
+ return hit == nullptr ? -1 : hit->tag();
49
+ }
50
+
51
+ } // namespace
52
+
53
+ TEST(hit_test_finds_the_view_under_a_point) {
54
+ Tree tree;
55
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
56
+ root->insertChild(tree.box(2, 50, 50, 100, 100), 0);
57
+
58
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
59
+ }
60
+
61
+ TEST(hit_test_uses_top_left_coordinates) {
62
+ Tree tree;
63
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
64
+ // Near the top of the root. On a coordinate system with the origin at the
65
+ // bottom -- AppKit's, unflipped -- this point would be over nothing.
66
+ root->insertChild(tree.box(2, 0, 0, 400, 40), 0);
67
+
68
+ EXPECT_EQ(tagAt(root, 200, 20), 2);
69
+ EXPECT_EQ(tagAt(root, 200, 380), 1);
70
+ }
71
+
72
+ TEST(hit_test_misses_return_the_root_not_a_child) {
73
+ Tree tree;
74
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
75
+ root->insertChild(tree.box(2, 50, 50, 100, 100), 0);
76
+
77
+ EXPECT_EQ(tagAt(root, 300, 300), 1);
78
+ }
79
+
80
+ TEST(hit_test_outside_the_root_is_a_miss) {
81
+ Tree tree;
82
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
83
+
84
+ EXPECT_EQ(tagAt(root, 500, 200), -1);
85
+ EXPECT_EQ(tagAt(root, -1, 200), -1);
86
+ }
87
+
88
+ TEST(hit_test_returns_the_deepest_view) {
89
+ Tree tree;
90
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
91
+ RnWin32View *middle = tree.box(2, 50, 50, 200, 200);
92
+ middle->insertChild(tree.box(3, 25, 25, 50, 50), 0);
93
+ root->insertChild(middle, 0);
94
+
95
+ // (100,100) is inside all three; the innermost wins.
96
+ EXPECT_EQ(tagAt(root, 100, 100), 3);
97
+ }
98
+
99
+ TEST(hit_test_respects_sibling_order) {
100
+ Tree tree;
101
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
102
+ root->insertChild(tree.box(2, 0, 0, 200, 200), 0);
103
+ root->insertChild(tree.box(3, 0, 0, 200, 200), 1);
104
+
105
+ // Exactly overlapping. The later child is painted on top, so it is the one a
106
+ // press lands on.
107
+ EXPECT_EQ(tagAt(root, 100, 100), 3);
108
+ }
109
+
110
+ TEST(hit_test_follows_z_index) {
111
+ Tree tree;
112
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
113
+ RnWin32View *under = tree.box(2, 0, 0, 200, 200);
114
+ RnWin32View *over = tree.box(3, 0, 0, 200, 200);
115
+ root->insertChild(under, 0);
116
+ root->insertChild(over, 1);
117
+
118
+ // A negative zIndex on the later child puts it underneath, and a press has to
119
+ // follow the paint order rather than the child list -- otherwise a view can
120
+ // be drawn behind another and still swallow its clicks.
121
+ over->setZIndex(-1);
122
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
123
+ }
124
+
125
+ TEST(hit_test_follows_a_scroll_offset) {
126
+ Tree tree;
127
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
128
+ RnWin32View *scroller = tree.box(50, 0, 0, 200, 200);
129
+ scroller->insertChild(tree.box(51, 0, 300, 200, 100), 0);
130
+ root->insertChild(scroller, 0);
131
+
132
+ // The row sits below the viewport, so nothing of it is under this point.
133
+ EXPECT_EQ(tagAt(root, 100, 50), 50);
134
+
135
+ scroller->setScrollOffset(0, 300);
136
+ EXPECT_EQ(tagAt(root, 100, 50), 51);
137
+ }
138
+
139
+ TEST(hit_test_skips_hidden_views) {
140
+ Tree tree;
141
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
142
+ RnWin32View *hidden = tree.box(2, 0, 0, 200, 200);
143
+ root->insertChild(hidden, 0);
144
+
145
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
146
+
147
+ // `display: none` takes a view out of painting and out of picking together.
148
+ // Distinct from opacity 0, which is still there to be pressed.
149
+ hidden->setHidden(true);
150
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
151
+ }
152
+
153
+ TEST(hit_test_skips_the_children_of_a_hidden_view) {
154
+ Tree tree;
155
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
156
+ RnWin32View *hidden = tree.box(2, 0, 0, 200, 200);
157
+ hidden->insertChild(tree.box(3, 0, 0, 100, 100), 0);
158
+ root->insertChild(hidden, 0);
159
+
160
+ hidden->setHidden(true);
161
+ EXPECT_EQ(tagAt(root, 50, 50), 1);
162
+ }
163
+
164
+ // `backfaceVisibility: 'hidden'` takes the view out of picking as well as out
165
+ // of the picture -- the back of a card is not clickable.
166
+ TEST(hit_test_skips_a_view_turned_away) {
167
+ Tree tree;
168
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
169
+ RnWin32View *card = tree.box(2, 0, 0, 200, 200);
170
+ card->setHidesBackFace(true);
171
+ root->insertChild(card, 0);
172
+
173
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
174
+
175
+ // Half a turn about Y, which mirrors it.
176
+ const float flipped[16] = {-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1};
177
+ card->setTransform(flipped);
178
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
179
+
180
+ // Turning the prop off shows it again. The early return this used to have
181
+ // left the view hidden for good.
182
+ card->setHidesBackFace(false);
183
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
184
+ }
185
+
186
+ // The two reasons a view can be hidden are independent, and Fabric applies
187
+ // them through different calls: props first, then layout metrics. So a card
188
+ // turned away from the viewer had `display: none`'s "no" written over the top
189
+ // of it on the very same mount, and came back.
190
+ TEST(display_none_and_a_back_face_do_not_cancel_each_other) {
191
+ Tree tree;
192
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
193
+ RnWin32View *card = tree.box(2, 0, 0, 200, 200);
194
+ card->setHidesBackFace(true);
195
+ root->insertChild(card, 0);
196
+
197
+ const float flipped[16] = {-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1};
198
+ card->setTransform(flipped);
199
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
200
+
201
+ // What Win32MountingManager says about every view that is not display: none.
202
+ card->setHidden(false);
203
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
204
+
205
+ // And the other way: facing the viewer must not reveal what the app hid.
206
+ card->setHidden(true);
207
+ card->setTransform(nullptr);
208
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
209
+ }
210
+
211
+ TEST(hit_test_follows_a_transform) {
212
+ Tree tree;
213
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
214
+ // The same wide, short card the paint tests rotate, at the same coordinates,
215
+ // so the two suites corroborate rather than each asserting its own idea of
216
+ // where the thing went.
217
+ RnWin32View *card = tree.box(2, 10, 40, 80, 20);
218
+ root->insertChild(card, 0);
219
+
220
+ EXPECT_EQ(tagAt(root, 20, 50), 2);
221
+ EXPECT_EQ(tagAt(root, 50, 20), 1);
222
+
223
+ // A quarter turn about the centre: the card now covers x 40..60, y 10..90.
224
+ const float quarterTurn[16] = {
225
+ 0, 1, 0, 0, //
226
+ -1, 0, 0, 0, //
227
+ 0, 0, 1, 0, //
228
+ 0, 0, 0, 1};
229
+ card->setTransform(quarterTurn);
230
+
231
+ // This is the assertion the whole inverse-matrix path exists for. A platform
232
+ // that draws the transform but does not pick through it leaves a rotated
233
+ // button clickable where it used to be, and both of these still pass the
234
+ // *first* way round -- which is why both are here.
235
+ EXPECT_EQ(tagAt(root, 50, 20), 2);
236
+ EXPECT_EQ(tagAt(root, 20, 50), 1);
237
+ }
238
+
239
+ TEST(hit_test_on_a_null_root_is_a_miss) {
240
+ EXPECT_EQ(tagAt(nullptr, 10, 10), -1);
241
+ }
242
+
243
+ // `pointerEvents`, which decides what a press can land on rather than what is
244
+ // drawn. The same four cases the GTK and AppKit suites ask, in the same order,
245
+ // because the answer has to be the same on all three.
246
+ TEST(pointer_events_none_passes_a_press_through_to_what_is_behind) {
247
+ Tree tree;
248
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
249
+ RnWin32View *behind = tree.box(10, 0, 0, 200, 200);
250
+ RnWin32View *over = tree.box(11, 0, 0, 200, 200);
251
+ root->insertChild(behind, 0);
252
+ root->insertChild(over, 1);
253
+
254
+ // Painted last, so it is on top and takes the press.
255
+ EXPECT_EQ(tagAt(root, 50, 50), 11);
256
+
257
+ over->setPointerEvents(RnWin32View::PointerEvents::None);
258
+ EXPECT_EQ(tagAt(root, 50, 50), 10);
259
+ }
260
+
261
+ TEST(pointer_events_none_takes_the_children_with_it) {
262
+ Tree tree;
263
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
264
+ RnWin32View *over = tree.box(11, 0, 0, 200, 200);
265
+ over->insertChild(tree.box(12, 0, 0, 100, 100), 0);
266
+ root->insertChild(over, 0);
267
+
268
+ EXPECT_EQ(tagAt(root, 50, 50), 12);
269
+ // The whole subtree leaves hit testing, not just the view the prop is on.
270
+ over->setPointerEvents(RnWin32View::PointerEvents::None);
271
+ EXPECT_EQ(tagAt(root, 50, 50), 1);
272
+ }
273
+
274
+ TEST(pointer_events_box_none_is_transparent_and_its_children_are_not) {
275
+ Tree tree;
276
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
277
+ RnWin32View *behind = tree.box(10, 0, 0, 300, 300);
278
+ RnWin32View *overlay = tree.box(11, 0, 0, 300, 300);
279
+ overlay->insertChild(tree.box(12, 0, 0, 100, 100), 0);
280
+ root->insertChild(behind, 0);
281
+ root->insertChild(overlay, 1);
282
+ overlay->setPointerEvents(RnWin32View::PointerEvents::BoxNone);
283
+
284
+ // Over the child: the overlay's children are still targets.
285
+ EXPECT_EQ(tagAt(root, 50, 50), 12);
286
+ // Over the overlay and nothing inside it: the press belongs to the view
287
+ // *behind*, not to the overlay's parent. That is the whole reason the mode
288
+ // exists, and returning the parent would look right until something was
289
+ // underneath.
290
+ EXPECT_EQ(tagAt(root, 200, 200), 10);
291
+ }
292
+
293
+ TEST(pointer_events_box_only_swallows_presses_meant_for_its_children) {
294
+ Tree tree;
295
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
296
+ RnWin32View *panel = tree.box(11, 0, 0, 300, 300);
297
+ panel->insertChild(tree.box(12, 0, 0, 100, 100), 0);
298
+ root->insertChild(panel, 0);
299
+
300
+ EXPECT_EQ(tagAt(root, 50, 50), 12);
301
+ panel->setPointerEvents(RnWin32View::PointerEvents::BoxOnly);
302
+ // The press lands on the panel even though it is over the child, which is
303
+ // what makes a disabled panel disable everything in it.
304
+ EXPECT_EQ(tagAt(root, 50, 50), 11);
305
+ EXPECT_EQ(tagAt(root, 200, 200), 11);
306
+ }
307
+
308
+ TEST(pointer_events_shows_up_in_the_tree_dump) {
309
+ Tree tree;
310
+ RnWin32View *view = tree.box(10, 0, 0, 100, 100);
311
+ view->setPointerEvents(RnWin32View::PointerEvents::BoxNone);
312
+ // Printed because it is invisible otherwise: a view with this prop is drawn
313
+ // exactly like one without, so the cross-host diff can only see the prop
314
+ // arrived if each host prints it.
315
+ EXPECT(view->describeTree().find("pe=box-none") != std::string::npos);
316
+ }
317
+
318
+ // --------------------------------------------------------------------------
319
+ // offsetPoint
320
+ // --------------------------------------------------------------------------
321
+ //
322
+ // `pageToLocal` is what `Touch::offsetPoint` is built from, and it inverts the
323
+ // same chain `hitTest` above inverts on the way down. The AppKit suite has the
324
+ // same five cases against `rnPageToLocal:fromRoot:into:`.
325
+
326
+ TEST(page_to_local_subtracts_the_ancestors) {
327
+ Tree tree;
328
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
329
+ RnWin32View *middle = tree.box(10, 50, 60, 300, 300);
330
+ RnWin32View *leaf = tree.box(20, 20, 30, 100, 100);
331
+ root->insertChild(middle, 0);
332
+ middle->insertChild(leaf, 0);
333
+
334
+ float x = 0.0f;
335
+ float y = 0.0f;
336
+ EXPECT(leaf->pageToLocal(root, 100.0f, 120.0f, x, y));
337
+ // 100 - 50 - 20, and 120 - 60 - 30.
338
+ EXPECT_NEAR(x, 30.0f, 0.001f);
339
+ EXPECT_NEAR(y, 30.0f, 0.001f);
340
+ }
341
+
342
+ // The root's own point is the page point, which is the case that made the old
343
+ // behaviour look right for as long as it did.
344
+ TEST(page_to_local_at_the_root_is_the_page_point) {
345
+ Tree tree;
346
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
347
+
348
+ float x = 0.0f;
349
+ float y = 0.0f;
350
+ EXPECT(root->pageToLocal(root, 100.0f, 120.0f, x, y));
351
+ EXPECT_NEAR(x, 100.0f, 0.001f);
352
+ EXPECT_NEAR(y, 120.0f, 0.001f);
353
+ }
354
+
355
+ TEST(page_to_local_follows_a_transformed_ancestor) {
356
+ Tree tree;
357
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
358
+ RnWin32View *moved = tree.box(10, 0, 0, 100, 100);
359
+ // Shifted 100 right and 50 down by a transform rather than by its frame.
360
+ const float translate[16] = {
361
+ 1, 0, 0, 0, //
362
+ 0, 1, 0, 0, //
363
+ 0, 0, 1, 0, //
364
+ 100, 50, 0, 1};
365
+ moved->setTransform(translate);
366
+ root->insertChild(moved, 0);
367
+
368
+ float x = 0.0f;
369
+ float y = 0.0f;
370
+ EXPECT(moved->pageToLocal(root, 110.0f, 60.0f, x, y));
371
+ // The press is 10 into the view as drawn, not 110.
372
+ EXPECT_NEAR(x, 10.0f, 0.001f);
373
+ EXPECT_NEAR(y, 10.0f, 0.001f);
374
+ }
375
+
376
+ // A scrolled ancestor moves its children, and the offset has to come back out
377
+ // -- the same offset `hitTest` adds on the way down.
378
+ TEST(page_to_local_follows_a_scrolled_ancestor) {
379
+ Tree tree;
380
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
381
+ RnWin32View *scroller = tree.box(10, 0, 0, 200, 200);
382
+ RnWin32View *row = tree.box(20, 0, 300, 200, 50);
383
+ root->insertChild(scroller, 0);
384
+ scroller->insertChild(row, 0);
385
+ scroller->setScrollOffset(0.0f, 280.0f);
386
+
387
+ float x = 0.0f;
388
+ float y = 0.0f;
389
+ // The row starts at 300 in content space and the view is scrolled 280, so it
390
+ // is drawn 20 down -- and a press there is at the row's own top.
391
+ EXPECT(row->pageToLocal(root, 10.0f, 20.0f, x, y));
392
+ EXPECT_NEAR(x, 10.0f, 0.001f);
393
+ EXPECT_NEAR(y, 0.0f, 0.001f);
394
+ }
395
+
396
+ TEST(page_to_local_refuses_a_chain_with_no_inverse) {
397
+ Tree tree;
398
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
399
+ RnWin32View *flat = tree.box(10, 0, 0, 100, 100);
400
+ const float flattened[16] = {
401
+ 0, 0, 0, 0, //
402
+ 0, 0, 0, 0, //
403
+ 0, 0, 1, 0, //
404
+ 0, 0, 0, 1};
405
+ flat->setTransform(flattened);
406
+ root->insertChild(flat, 0);
407
+
408
+ float x = -1.0f;
409
+ float y = -1.0f;
410
+ EXPECT(!flat->pageToLocal(root, 10.0f, 10.0f, x, y));
411
+ // Untouched, so the caller's fallback is what is used.
412
+ EXPECT_NEAR(x, -1.0f, 0.001f);
413
+ }
414
+
415
+ // ---------------------------------------------------------------------------
416
+ // `hitSlop`: how far outside its own box a view answers a press.
417
+ //
418
+ // The same four questions tests/test_hittest.cpp asks of GTK, in the same
419
+ // order, and asserted the same way: through the hit test every press goes
420
+ // through rather than against the stored insets, because a view that remembered
421
+ // its slop and did not widen the test would leave every target the size it was.
422
+ // ---------------------------------------------------------------------------
423
+
424
+ TEST(hit_slop_grows_the_target_outside_the_box) {
425
+ Tree tree;
426
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
427
+ RnWin32View *icon = tree.box(60, 100, 100, 20, 20);
428
+ root->insertChild(icon, 0);
429
+ const float insets[4] = {12.0f, 12.0f, 12.0f, 12.0f};
430
+ icon->setHitSlop(insets);
431
+
432
+ // Inside the box, as before.
433
+ EXPECT_EQ(tagAt(root, 110, 110), 60);
434
+ // Ten points outside it on each side, which is inside the slop.
435
+ EXPECT_EQ(tagAt(root, 92, 110), 60);
436
+ EXPECT_EQ(tagAt(root, 128, 110), 60);
437
+ EXPECT_EQ(tagAt(root, 110, 92), 60);
438
+ EXPECT_EQ(tagAt(root, 110, 128), 60);
439
+ // And past it, which is the root again.
440
+ EXPECT_EQ(tagAt(root, 80, 110), 1);
441
+ }
442
+
443
+ // Each edge on its own, because four numbers in one struct is four chances to
444
+ // read one into the wrong side: a slop that grew the top when the app asked for
445
+ // the bottom would pass any symmetric test.
446
+ TEST(hit_slop_applies_each_edge_where_it_was_asked_for) {
447
+ Tree tree;
448
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
449
+ RnWin32View *view = tree.box(61, 100, 100, 20, 20);
450
+ root->insertChild(view, 0);
451
+
452
+ const float top[4] = {15.0f, 0.0f, 0.0f, 0.0f};
453
+ view->setHitSlop(top);
454
+ EXPECT_EQ(tagAt(root, 110, 90), 61);
455
+ // Not the other three.
456
+ EXPECT_EQ(tagAt(root, 110, 130), 1);
457
+ EXPECT_EQ(tagAt(root, 90, 110), 1);
458
+ EXPECT_EQ(tagAt(root, 130, 110), 1);
459
+
460
+ // And the opposite edge, through the same view: the prop can change.
461
+ const float bottom[4] = {0.0f, 0.0f, 15.0f, 0.0f};
462
+ view->setHitSlop(bottom);
463
+ EXPECT_EQ(tagAt(root, 110, 130), 61);
464
+ EXPECT_EQ(tagAt(root, 110, 90), 1);
465
+ }
466
+
467
+ // Taken away again. A slop left behind is a view that swallows presses meant for
468
+ // its neighbour, which is harder to notice than a target that is too small.
469
+ TEST(hit_slop_can_be_taken_away_again) {
470
+ Tree tree;
471
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
472
+ RnWin32View *view = tree.box(62, 100, 100, 20, 20);
473
+ root->insertChild(view, 0);
474
+
475
+ const float insets[4] = {12.0f, 12.0f, 12.0f, 12.0f};
476
+ view->setHitSlop(insets);
477
+ EXPECT_EQ(tagAt(root, 92, 110), 62);
478
+
479
+ view->setHitSlop(nullptr);
480
+ EXPECT_EQ(tagAt(root, 92, 110), 1);
481
+ EXPECT_EQ(tagAt(root, 110, 110), 62);
482
+ }
483
+
484
+ // A slop that overlaps a sibling does not win over it: the sibling is drawn on
485
+ // top, and a target that reached under something visible would take presses
486
+ // meant for it.
487
+ TEST(hit_slop_does_not_beat_a_view_drawn_over_it) {
488
+ Tree tree;
489
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
490
+ RnWin32View *first = tree.box(63, 100, 100, 20, 20);
491
+ root->insertChild(first, 0);
492
+ const float insets[4] = {0.0f, 40.0f, 0.0f, 0.0f};
493
+ first->setHitSlop(insets);
494
+
495
+ RnWin32View *second = tree.box(64, 130, 100, 20, 20);
496
+ root->insertChild(second, 1);
497
+
498
+ // Inside the slop and outside the sibling: the slop answers.
499
+ EXPECT_EQ(tagAt(root, 125, 110), 63);
500
+ // Inside both: the sibling, which is on top.
501
+ EXPECT_EQ(tagAt(root, 140, 110), 64);
502
+ }
503
+
504
+ // And it is in the tree dump, which is the only way to see it: a view with a
505
+ // bigger target is drawn exactly like one without. Spelled as the other two
506
+ // hosts spell it, because the dump is diffed across the three.
507
+ TEST(hit_slop_is_reported_in_the_tree) {
508
+ Tree tree;
509
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
510
+ RnWin32View *view = tree.box(65, 100, 100, 20, 20);
511
+ root->insertChild(view, 0);
512
+
513
+ EXPECT(root->describeTree().find("hit-slop=") == std::string::npos);
514
+
515
+ const float insets[4] = {16.0f, 8.0f, 4.0f, 2.0f};
516
+ view->setHitSlop(insets);
517
+ EXPECT(root->describeTree().find("hit-slop=(16,8,4,2)") != std::string::npos);
518
+
519
+ // Zeroes are not a slop, so nothing is printed for a view that asked for
520
+ // none: the other two hosts print nothing there either.
521
+ const float none[4] = {0.0f, 0.0f, 0.0f, 0.0f};
522
+ view->setHitSlop(none);
523
+ EXPECT(root->describeTree().find("hit-slop=") == std::string::npos);
524
+ }
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // The `cursor` style property, which is a hit-test question on this platform.
528
+ //
529
+ // Win32 has no per-view cursor: the window answers `WM_SETCURSOR` with whatever
530
+ // is under the pointer, so "which cursor" is "which view", and that is what
531
+ // these ask. The GTK and AppKit hosts hand the keyword to their toolkit and get
532
+ // the inheritance for free, which is why this is the only suite that has to say
533
+ // what inheritance means.
534
+ // ---------------------------------------------------------------------------
535
+
536
+ TEST(cursor_is_the_innermost_view_that_asked_for_one) {
537
+ Tree tree;
538
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
539
+ RnWin32View *button = tree.box(2, 50, 50, 100, 100);
540
+ RnWin32View *label = tree.box(3, 10, 10, 20, 20);
541
+ button->setCursor("pointer");
542
+ label->setCursor("text");
543
+ button->insertChild(label, 0);
544
+ root->insertChild(button, 0);
545
+
546
+ // Over the label: its own keyword.
547
+ EXPECT_EQ(basalt::win32::cursorNameAt(root, 70, 70), std::string("text"));
548
+ // Over the button but not the label: the button's.
549
+ EXPECT_EQ(basalt::win32::cursorNameAt(root, 120, 120), std::string("pointer"));
550
+ // Over neither: nothing, which is what leaves the pointer alone.
551
+ EXPECT(basalt::win32::cursorNameAt(root, 300, 300).empty());
552
+ }
553
+
554
+ // CSS's cursor inherits, so a child that asked for nothing shows its parent's.
555
+ // A host that only read the view under the pointer would answer nothing here,
556
+ // which is an arrow over the middle of a button.
557
+ TEST(cursor_is_inherited_by_a_child_that_asked_for_nothing) {
558
+ Tree tree;
559
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
560
+ RnWin32View *button = tree.box(2, 50, 50, 100, 100);
561
+ RnWin32View *label = tree.box(3, 10, 10, 20, 20);
562
+ button->setCursor("pointer");
563
+ button->insertChild(label, 0);
564
+ root->insertChild(button, 0);
565
+
566
+ EXPECT_EQ(basalt::win32::cursorNameAt(root, 70, 70), std::string("pointer"));
567
+
568
+ // And taken away again: `cursor: 'auto'` arrives as no keyword at all.
569
+ button->setCursor(nullptr);
570
+ EXPECT(basalt::win32::cursorNameAt(root, 70, 70).empty());
571
+ }
572
+
573
+ // A view a press passes through does not change the pointer either, which falls
574
+ // out of using the same hit test for both and is worth pinning: the two must
575
+ // not disagree about what the pointer is over.
576
+ TEST(cursor_follows_pointer_events_like_a_press_does) {
577
+ Tree tree;
578
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
579
+ RnWin32View *under = tree.box(2, 50, 50, 100, 100);
580
+ RnWin32View *over = tree.box(3, 50, 50, 100, 100);
581
+ under->setCursor("pointer");
582
+ over->setCursor("text");
583
+ over->setPointerEvents(RnWin32View::PointerEvents::None);
584
+ root->insertChild(under, 0);
585
+ root->insertChild(over, 1);
586
+
587
+ EXPECT_EQ(basalt::win32::cursorNameAt(root, 70, 70), std::string("pointer"));
588
+ }
589
+
590
+ TEST(cursor_is_reported_in_the_tree) {
591
+ Tree tree;
592
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
593
+ EXPECT(root->describeTree().find("cursor=") == std::string::npos);
594
+
595
+ root->setCursor("ns-resize");
596
+ // The keyword, as the other two hosts print it: one end-to-end scenario reads
597
+ // this line on all three, and the hyphenated name is where a mapping table
598
+ // goes wrong.
599
+ EXPECT(root->describeTree().find("cursor=ns-resize") != std::string::npos);
600
+ }
601
+
602
+ // And the Win32 half: a keyword as a cursor the system has.
603
+ //
604
+ // Against `LoadCursorW` rather than a table of names, because the table is the
605
+ // thing under test: a keyword that landed on a cursor the system does not have
606
+ // answers null, and the assertions below would pass on an empty map if they
607
+ // only checked that nothing crashed. So each group is also compared against the
608
+ // others, which is what says `ns-resize` and `ew-resize` are not the same arrow.
609
+ TEST(cursor_keywords_map_onto_the_cursors_windows_has) {
610
+ const auto loaded = [](const char *keyword) {
611
+ return basalt::win32::win32CursorFor(keyword).cursor;
612
+ };
613
+
614
+ EXPECT(loaded("default") == LoadCursorW(nullptr, IDC_ARROW));
615
+ EXPECT(loaded("pointer") == LoadCursorW(nullptr, IDC_HAND));
616
+ EXPECT(loaded("text") == LoadCursorW(nullptr, IDC_IBEAM));
617
+ EXPECT(loaded("crosshair") == LoadCursorW(nullptr, IDC_CROSS));
618
+ EXPECT(loaded("wait") == LoadCursorW(nullptr, IDC_WAIT));
619
+ EXPECT(loaded("progress") == LoadCursorW(nullptr, IDC_APPSTARTING));
620
+ EXPECT(loaded("help") == LoadCursorW(nullptr, IDC_HELP));
621
+ EXPECT(loaded("not-allowed") == LoadCursorW(nullptr, IDC_NO));
622
+
623
+ // The families, which share a cursor because Windows draws no difference --
624
+ // and the eight resize keywords collapse onto four arrows the way a window's
625
+ // own edges do.
626
+ EXPECT(loaded("grab") == loaded("move"));
627
+ EXPECT(loaded("grabbing") == loaded("all-scroll"));
628
+ EXPECT(loaded("ew-resize") == loaded("col-resize"));
629
+ EXPECT(loaded("ns-resize") == loaded("row-resize"));
630
+ EXPECT(loaded("ne-resize") == loaded("nesw-resize"));
631
+ EXPECT(loaded("nw-resize") == loaded("nwse-resize"));
632
+
633
+ // And the four resize arrows are four different arrows.
634
+ EXPECT(loaded("ew-resize") != loaded("ns-resize"));
635
+ EXPECT(loaded("nesw-resize") != loaded("nwse-resize"));
636
+ EXPECT(loaded("ew-resize") != loaded("nesw-resize"));
637
+ }
638
+
639
+ TEST(cursor_none_hides_the_pointer_rather_than_loading_one) {
640
+ const basalt::win32::Win32Cursor none = basalt::win32::win32CursorFor("none");
641
+ EXPECT(none.hidden);
642
+ EXPECT(none.cursor == nullptr);
643
+
644
+ // No keyword at all is neither: the pointer is left as it is.
645
+ const basalt::win32::Win32Cursor nothing = basalt::win32::win32CursorFor("");
646
+ EXPECT(!nothing.hidden);
647
+ EXPECT(nothing.cursor == nullptr);
648
+ }
649
+
650
+ // The keywords the stock set has nothing for, which this platform draws: see
651
+ // Win32Cursors.cpp. They used to answer null, which left the pointer an arrow
652
+ // and made `cursor` look unimplemented for six of its values.
653
+ //
654
+ // Each is a cursor of its own rather than a shared one, which is the claim
655
+ // that fails if the drawing stops happening -- a null answer, or one cursor
656
+ // handed back for all six, would both show up here. What they *look* like is
657
+ // not assertable from a handle, and nobody has seen them on a screen yet;
658
+ // backlog/platform-windows.md says so.
659
+ TEST(cursor_a_keyword_windows_has_nothing_for_is_drawn_here) {
660
+ std::vector<HCURSOR> drawn;
661
+ for (const char *keyword : {"alias", "cell", "context-menu", "copy", "zoom-in", "zoom-out"}) {
662
+ const basalt::win32::Win32Cursor answer = basalt::win32::win32CursorFor(keyword);
663
+ EXPECT(answer.cursor != nullptr);
664
+ EXPECT(!answer.hidden);
665
+ EXPECT(answer.cursor != LoadCursorW(nullptr, IDC_ARROW));
666
+ for (HCURSOR other : drawn) {
667
+ EXPECT(answer.cursor != other);
668
+ }
669
+ drawn.push_back(answer.cursor);
670
+ }
671
+
672
+ // Asked for twice, answered with the same handle: these are made once and
673
+ // kept, where a stock cursor is looked up in a table the system owns. A
674
+ // cursor made per call would leak one per mouse move.
675
+ EXPECT(basalt::win32::win32CursorFor("zoom-in").cursor
676
+ == basalt::win32::win32CursorFor("zoom-in").cursor);
677
+
678
+ // And a keyword that is neither stock nor drawn still leaves the pointer
679
+ // alone, which is what an unknown value has to do.
680
+ EXPECT(basalt::win32::win32CursorFor("not-a-cursor").cursor == nullptr);
681
+ }
682
+
683
+ // ---------------------------------------------------------------------------
684
+ // `keyboardType` as a Windows input scope. See Win32InputScopes.h.
685
+ //
686
+ // A table, so a table test: what is worth asserting is that each name lands on
687
+ // the scope that means the same thing, and that the ones Windows draws no
688
+ // distinction between are the ones that share a scope. Here rather than in the
689
+ // <TextInput> suite because the mapping carries no React Native, which is also
690
+ // why it is keyed on the name core prints rather than on the enum.
691
+ // ---------------------------------------------------------------------------
692
+
693
+ TEST(input_scope_follows_the_keyboard_type_a_field_asked_for) {
694
+ const auto scope = [](const char *name) {
695
+ return basalt::win32::inputScopeForKeyboardType(name);
696
+ };
697
+
698
+ EXPECT_EQ(static_cast<int>(scope("email-address")),
699
+ static_cast<int>(IS_EMAIL_SMTPEMAILADDRESS));
700
+ EXPECT_EQ(static_cast<int>(scope("url")), static_cast<int>(IS_URL));
701
+ EXPECT_EQ(static_cast<int>(scope("phone-pad")),
702
+ static_cast<int>(IS_TELEPHONE_FULLTELEPHONENUMBER));
703
+ // A number that may carry a separator against digits and nothing else, which
704
+ // is the one distinction in this table that is not just a rename.
705
+ EXPECT_EQ(static_cast<int>(scope("numeric")), static_cast<int>(IS_NUMBER));
706
+ EXPECT_EQ(static_cast<int>(scope("decimal-pad")), static_cast<int>(IS_NUMBER));
707
+ EXPECT_EQ(static_cast<int>(scope("number-pad")), static_cast<int>(IS_DIGITS));
708
+ EXPECT(scope("numeric") != scope("number-pad"));
709
+
710
+ // `default` and the names that are iOS's own vocabulary: ordinary text, which
711
+ // is more useful than refusing to answer and is what the comment there says.
712
+ EXPECT_EQ(static_cast<int>(scope("default")), static_cast<int>(IS_DEFAULT));
713
+ EXPECT_EQ(static_cast<int>(scope("twitter")), static_cast<int>(IS_DEFAULT));
714
+ EXPECT_EQ(static_cast<int>(scope("ascii-capable")), static_cast<int>(IS_DEFAULT));
715
+ // And a name this table has never heard of, which is what a future React
716
+ // Native adding one looks like.
717
+ EXPECT_EQ(static_cast<int>(scope("something-new")), static_cast<int>(IS_DEFAULT));
718
+ }
719
+
720
+ // What a field asked for about its text reaches the tree dump, which is where
721
+ // an app can see that a prop arrived on a host that cannot act on it -- and
722
+ // what the end-to-end scenario reads on all three.
723
+ TEST(input_kinds_and_text_checking_are_reported_in_the_tree) {
724
+ Tree tree;
725
+ RnWin32View *view = tree.box(1, 0, 0, 100, 40);
726
+
727
+ // Nothing said: nothing printed. Unset spelling is a third state rather than
728
+ // off, and a host that resolved it to false would mark every field.
729
+ EXPECT(view->describeTree().find("spellcheck=") == std::string::npos);
730
+ EXPECT(view->describeTree().find("autocorrect=") == std::string::npos);
731
+ EXPECT(view->describeTree().find("keyboard=") == std::string::npos);
732
+
733
+ view->setTextChecking("off", "off");
734
+ view->setInputKinds("none", "email-address");
735
+ const std::string dump = view->describeTree();
736
+ EXPECT(dump.find("spellcheck=off") != std::string::npos);
737
+ EXPECT(dump.find("autocorrect=off") != std::string::npos);
738
+ EXPECT(dump.find("autocapitalize=none") != std::string::npos);
739
+ EXPECT(dump.find("keyboard=email-address") != std::string::npos);
740
+
741
+ // And taken away again, which is what a field that stops saying anything
742
+ // means: core answers a null name for an unset flag.
743
+ view->setTextChecking(nullptr, nullptr);
744
+ const std::string quiet = view->describeTree();
745
+ EXPECT(quiet.find("spellcheck=") == std::string::npos);
746
+ EXPECT(quiet.find("autocorrect=") == std::string::npos);
747
+ // The other two are plain enums with React Native's own defaults, so they
748
+ // stay: a field always has a capitalisation and a keyboard.
749
+ EXPECT(quiet.find("autocapitalize=none") != std::string::npos);
750
+ }