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,410 @@
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
+
18
+ #include <memory>
19
+ #include <sstream>
20
+ #include <vector>
21
+
22
+ using basalt::win32::hitTest;
23
+ using basalt::win32::RnWin32View;
24
+
25
+ namespace {
26
+
27
+ class Tree {
28
+ public:
29
+ RnWin32View *box(int32_t tag, float x, float y, float w, float h) {
30
+ auto view = std::make_unique<RnWin32View>(tag);
31
+ view->setFrame(x, y, w, h);
32
+ RnWin32View *raw = view.get();
33
+ views_.push_back(std::move(view));
34
+ return raw;
35
+ }
36
+
37
+ private:
38
+ std::vector<std::unique_ptr<RnWin32View>> views_;
39
+ };
40
+
41
+ // The tag of whatever is under a point, or -1 for a miss. Comparing tags rather
42
+ // than pointers makes a failure say which view it found.
43
+ int32_t tagAt(RnWin32View *root, float x, float y) {
44
+ RnWin32View *hit = hitTest(root, x, y);
45
+ return hit == nullptr ? -1 : hit->tag();
46
+ }
47
+
48
+ } // namespace
49
+
50
+ TEST(hit_test_finds_the_view_under_a_point) {
51
+ Tree tree;
52
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
53
+ root->insertChild(tree.box(2, 50, 50, 100, 100), 0);
54
+
55
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
56
+ }
57
+
58
+ TEST(hit_test_uses_top_left_coordinates) {
59
+ Tree tree;
60
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
61
+ // Near the top of the root. On a coordinate system with the origin at the
62
+ // bottom -- AppKit's, unflipped -- this point would be over nothing.
63
+ root->insertChild(tree.box(2, 0, 0, 400, 40), 0);
64
+
65
+ EXPECT_EQ(tagAt(root, 200, 20), 2);
66
+ EXPECT_EQ(tagAt(root, 200, 380), 1);
67
+ }
68
+
69
+ TEST(hit_test_misses_return_the_root_not_a_child) {
70
+ Tree tree;
71
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
72
+ root->insertChild(tree.box(2, 50, 50, 100, 100), 0);
73
+
74
+ EXPECT_EQ(tagAt(root, 300, 300), 1);
75
+ }
76
+
77
+ TEST(hit_test_outside_the_root_is_a_miss) {
78
+ Tree tree;
79
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
80
+
81
+ EXPECT_EQ(tagAt(root, 500, 200), -1);
82
+ EXPECT_EQ(tagAt(root, -1, 200), -1);
83
+ }
84
+
85
+ TEST(hit_test_returns_the_deepest_view) {
86
+ Tree tree;
87
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
88
+ RnWin32View *middle = tree.box(2, 50, 50, 200, 200);
89
+ middle->insertChild(tree.box(3, 25, 25, 50, 50), 0);
90
+ root->insertChild(middle, 0);
91
+
92
+ // (100,100) is inside all three; the innermost wins.
93
+ EXPECT_EQ(tagAt(root, 100, 100), 3);
94
+ }
95
+
96
+ TEST(hit_test_respects_sibling_order) {
97
+ Tree tree;
98
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
99
+ root->insertChild(tree.box(2, 0, 0, 200, 200), 0);
100
+ root->insertChild(tree.box(3, 0, 0, 200, 200), 1);
101
+
102
+ // Exactly overlapping. The later child is painted on top, so it is the one a
103
+ // press lands on.
104
+ EXPECT_EQ(tagAt(root, 100, 100), 3);
105
+ }
106
+
107
+ TEST(hit_test_follows_z_index) {
108
+ Tree tree;
109
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
110
+ RnWin32View *under = tree.box(2, 0, 0, 200, 200);
111
+ RnWin32View *over = tree.box(3, 0, 0, 200, 200);
112
+ root->insertChild(under, 0);
113
+ root->insertChild(over, 1);
114
+
115
+ // A negative zIndex on the later child puts it underneath, and a press has to
116
+ // follow the paint order rather than the child list -- otherwise a view can
117
+ // be drawn behind another and still swallow its clicks.
118
+ over->setZIndex(-1);
119
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
120
+ }
121
+
122
+ TEST(hit_test_follows_a_scroll_offset) {
123
+ Tree tree;
124
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
125
+ RnWin32View *scroller = tree.box(50, 0, 0, 200, 200);
126
+ scroller->insertChild(tree.box(51, 0, 300, 200, 100), 0);
127
+ root->insertChild(scroller, 0);
128
+
129
+ // The row sits below the viewport, so nothing of it is under this point.
130
+ EXPECT_EQ(tagAt(root, 100, 50), 50);
131
+
132
+ scroller->setScrollOffset(0, 300);
133
+ EXPECT_EQ(tagAt(root, 100, 50), 51);
134
+ }
135
+
136
+ TEST(hit_test_skips_hidden_views) {
137
+ Tree tree;
138
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
139
+ RnWin32View *hidden = tree.box(2, 0, 0, 200, 200);
140
+ root->insertChild(hidden, 0);
141
+
142
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
143
+
144
+ // `display: none` takes a view out of painting and out of picking together.
145
+ // Distinct from opacity 0, which is still there to be pressed.
146
+ hidden->setHidden(true);
147
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
148
+ }
149
+
150
+ TEST(hit_test_skips_the_children_of_a_hidden_view) {
151
+ Tree tree;
152
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
153
+ RnWin32View *hidden = tree.box(2, 0, 0, 200, 200);
154
+ hidden->insertChild(tree.box(3, 0, 0, 100, 100), 0);
155
+ root->insertChild(hidden, 0);
156
+
157
+ hidden->setHidden(true);
158
+ EXPECT_EQ(tagAt(root, 50, 50), 1);
159
+ }
160
+
161
+ // `backfaceVisibility: 'hidden'` takes the view out of picking as well as out
162
+ // of the picture -- the back of a card is not clickable.
163
+ TEST(hit_test_skips_a_view_turned_away) {
164
+ Tree tree;
165
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
166
+ RnWin32View *card = tree.box(2, 0, 0, 200, 200);
167
+ card->setHidesBackFace(true);
168
+ root->insertChild(card, 0);
169
+
170
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
171
+
172
+ // Half a turn about Y, which mirrors it.
173
+ const float flipped[16] = {-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1};
174
+ card->setTransform(flipped);
175
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
176
+
177
+ // Turning the prop off shows it again. The early return this used to have
178
+ // left the view hidden for good.
179
+ card->setHidesBackFace(false);
180
+ EXPECT_EQ(tagAt(root, 100, 100), 2);
181
+ }
182
+
183
+ // The two reasons a view can be hidden are independent, and Fabric applies
184
+ // them through different calls: props first, then layout metrics. So a card
185
+ // turned away from the viewer had `display: none`'s "no" written over the top
186
+ // of it on the very same mount, and came back.
187
+ TEST(display_none_and_a_back_face_do_not_cancel_each_other) {
188
+ Tree tree;
189
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
190
+ RnWin32View *card = tree.box(2, 0, 0, 200, 200);
191
+ card->setHidesBackFace(true);
192
+ root->insertChild(card, 0);
193
+
194
+ const float flipped[16] = {-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1};
195
+ card->setTransform(flipped);
196
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
197
+
198
+ // What Win32MountingManager says about every view that is not display: none.
199
+ card->setHidden(false);
200
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
201
+
202
+ // And the other way: facing the viewer must not reveal what the app hid.
203
+ card->setHidden(true);
204
+ card->setTransform(nullptr);
205
+ EXPECT_EQ(tagAt(root, 100, 100), 1);
206
+ }
207
+
208
+ TEST(hit_test_follows_a_transform) {
209
+ Tree tree;
210
+ RnWin32View *root = tree.box(1, 0, 0, 100, 100);
211
+ // The same wide, short card the paint tests rotate, at the same coordinates,
212
+ // so the two suites corroborate rather than each asserting its own idea of
213
+ // where the thing went.
214
+ RnWin32View *card = tree.box(2, 10, 40, 80, 20);
215
+ root->insertChild(card, 0);
216
+
217
+ EXPECT_EQ(tagAt(root, 20, 50), 2);
218
+ EXPECT_EQ(tagAt(root, 50, 20), 1);
219
+
220
+ // A quarter turn about the centre: the card now covers x 40..60, y 10..90.
221
+ const float quarterTurn[16] = {
222
+ 0, 1, 0, 0, //
223
+ -1, 0, 0, 0, //
224
+ 0, 0, 1, 0, //
225
+ 0, 0, 0, 1};
226
+ card->setTransform(quarterTurn);
227
+
228
+ // This is the assertion the whole inverse-matrix path exists for. A platform
229
+ // that draws the transform but does not pick through it leaves a rotated
230
+ // button clickable where it used to be, and both of these still pass the
231
+ // *first* way round -- which is why both are here.
232
+ EXPECT_EQ(tagAt(root, 50, 20), 2);
233
+ EXPECT_EQ(tagAt(root, 20, 50), 1);
234
+ }
235
+
236
+ TEST(hit_test_on_a_null_root_is_a_miss) {
237
+ EXPECT_EQ(tagAt(nullptr, 10, 10), -1);
238
+ }
239
+
240
+ // `pointerEvents`, which decides what a press can land on rather than what is
241
+ // drawn. The same four cases the GTK and AppKit suites ask, in the same order,
242
+ // because the answer has to be the same on all three.
243
+ TEST(pointer_events_none_passes_a_press_through_to_what_is_behind) {
244
+ Tree tree;
245
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
246
+ RnWin32View *behind = tree.box(10, 0, 0, 200, 200);
247
+ RnWin32View *over = tree.box(11, 0, 0, 200, 200);
248
+ root->insertChild(behind, 0);
249
+ root->insertChild(over, 1);
250
+
251
+ // Painted last, so it is on top and takes the press.
252
+ EXPECT_EQ(tagAt(root, 50, 50), 11);
253
+
254
+ over->setPointerEvents(RnWin32View::PointerEvents::None);
255
+ EXPECT_EQ(tagAt(root, 50, 50), 10);
256
+ }
257
+
258
+ TEST(pointer_events_none_takes_the_children_with_it) {
259
+ Tree tree;
260
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
261
+ RnWin32View *over = tree.box(11, 0, 0, 200, 200);
262
+ over->insertChild(tree.box(12, 0, 0, 100, 100), 0);
263
+ root->insertChild(over, 0);
264
+
265
+ EXPECT_EQ(tagAt(root, 50, 50), 12);
266
+ // The whole subtree leaves hit testing, not just the view the prop is on.
267
+ over->setPointerEvents(RnWin32View::PointerEvents::None);
268
+ EXPECT_EQ(tagAt(root, 50, 50), 1);
269
+ }
270
+
271
+ TEST(pointer_events_box_none_is_transparent_and_its_children_are_not) {
272
+ Tree tree;
273
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
274
+ RnWin32View *behind = tree.box(10, 0, 0, 300, 300);
275
+ RnWin32View *overlay = tree.box(11, 0, 0, 300, 300);
276
+ overlay->insertChild(tree.box(12, 0, 0, 100, 100), 0);
277
+ root->insertChild(behind, 0);
278
+ root->insertChild(overlay, 1);
279
+ overlay->setPointerEvents(RnWin32View::PointerEvents::BoxNone);
280
+
281
+ // Over the child: the overlay's children are still targets.
282
+ EXPECT_EQ(tagAt(root, 50, 50), 12);
283
+ // Over the overlay and nothing inside it: the press belongs to the view
284
+ // *behind*, not to the overlay's parent. That is the whole reason the mode
285
+ // exists, and returning the parent would look right until something was
286
+ // underneath.
287
+ EXPECT_EQ(tagAt(root, 200, 200), 10);
288
+ }
289
+
290
+ TEST(pointer_events_box_only_swallows_presses_meant_for_its_children) {
291
+ Tree tree;
292
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
293
+ RnWin32View *panel = tree.box(11, 0, 0, 300, 300);
294
+ panel->insertChild(tree.box(12, 0, 0, 100, 100), 0);
295
+ root->insertChild(panel, 0);
296
+
297
+ EXPECT_EQ(tagAt(root, 50, 50), 12);
298
+ panel->setPointerEvents(RnWin32View::PointerEvents::BoxOnly);
299
+ // The press lands on the panel even though it is over the child, which is
300
+ // what makes a disabled panel disable everything in it.
301
+ EXPECT_EQ(tagAt(root, 50, 50), 11);
302
+ EXPECT_EQ(tagAt(root, 200, 200), 11);
303
+ }
304
+
305
+ TEST(pointer_events_shows_up_in_the_tree_dump) {
306
+ Tree tree;
307
+ RnWin32View *view = tree.box(10, 0, 0, 100, 100);
308
+ view->setPointerEvents(RnWin32View::PointerEvents::BoxNone);
309
+ // Printed because it is invisible otherwise: a view with this prop is drawn
310
+ // exactly like one without, so the cross-host diff can only see the prop
311
+ // arrived if each host prints it.
312
+ EXPECT(view->describeTree().find("pe=box-none") != std::string::npos);
313
+ }
314
+
315
+ // --------------------------------------------------------------------------
316
+ // offsetPoint
317
+ // --------------------------------------------------------------------------
318
+ //
319
+ // `pageToLocal` is what `Touch::offsetPoint` is built from, and it inverts the
320
+ // same chain `hitTest` above inverts on the way down. The AppKit suite has the
321
+ // same five cases against `rnPageToLocal:fromRoot:into:`.
322
+
323
+ TEST(page_to_local_subtracts_the_ancestors) {
324
+ Tree tree;
325
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
326
+ RnWin32View *middle = tree.box(10, 50, 60, 300, 300);
327
+ RnWin32View *leaf = tree.box(20, 20, 30, 100, 100);
328
+ root->insertChild(middle, 0);
329
+ middle->insertChild(leaf, 0);
330
+
331
+ float x = 0.0f;
332
+ float y = 0.0f;
333
+ EXPECT(leaf->pageToLocal(root, 100.0f, 120.0f, x, y));
334
+ // 100 - 50 - 20, and 120 - 60 - 30.
335
+ EXPECT_NEAR(x, 30.0f, 0.001f);
336
+ EXPECT_NEAR(y, 30.0f, 0.001f);
337
+ }
338
+
339
+ // The root's own point is the page point, which is the case that made the old
340
+ // behaviour look right for as long as it did.
341
+ TEST(page_to_local_at_the_root_is_the_page_point) {
342
+ Tree tree;
343
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
344
+
345
+ float x = 0.0f;
346
+ float y = 0.0f;
347
+ EXPECT(root->pageToLocal(root, 100.0f, 120.0f, x, y));
348
+ EXPECT_NEAR(x, 100.0f, 0.001f);
349
+ EXPECT_NEAR(y, 120.0f, 0.001f);
350
+ }
351
+
352
+ TEST(page_to_local_follows_a_transformed_ancestor) {
353
+ Tree tree;
354
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
355
+ RnWin32View *moved = tree.box(10, 0, 0, 100, 100);
356
+ // Shifted 100 right and 50 down by a transform rather than by its frame.
357
+ const float translate[16] = {
358
+ 1, 0, 0, 0, //
359
+ 0, 1, 0, 0, //
360
+ 0, 0, 1, 0, //
361
+ 100, 50, 0, 1};
362
+ moved->setTransform(translate);
363
+ root->insertChild(moved, 0);
364
+
365
+ float x = 0.0f;
366
+ float y = 0.0f;
367
+ EXPECT(moved->pageToLocal(root, 110.0f, 60.0f, x, y));
368
+ // The press is 10 into the view as drawn, not 110.
369
+ EXPECT_NEAR(x, 10.0f, 0.001f);
370
+ EXPECT_NEAR(y, 10.0f, 0.001f);
371
+ }
372
+
373
+ // A scrolled ancestor moves its children, and the offset has to come back out
374
+ // -- the same offset `hitTest` adds on the way down.
375
+ TEST(page_to_local_follows_a_scrolled_ancestor) {
376
+ Tree tree;
377
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
378
+ RnWin32View *scroller = tree.box(10, 0, 0, 200, 200);
379
+ RnWin32View *row = tree.box(20, 0, 300, 200, 50);
380
+ root->insertChild(scroller, 0);
381
+ scroller->insertChild(row, 0);
382
+ scroller->setScrollOffset(0.0f, 280.0f);
383
+
384
+ float x = 0.0f;
385
+ float y = 0.0f;
386
+ // The row starts at 300 in content space and the view is scrolled 280, so it
387
+ // is drawn 20 down -- and a press there is at the row's own top.
388
+ EXPECT(row->pageToLocal(root, 10.0f, 20.0f, x, y));
389
+ EXPECT_NEAR(x, 10.0f, 0.001f);
390
+ EXPECT_NEAR(y, 0.0f, 0.001f);
391
+ }
392
+
393
+ TEST(page_to_local_refuses_a_chain_with_no_inverse) {
394
+ Tree tree;
395
+ RnWin32View *root = tree.box(1, 0, 0, 400, 400);
396
+ RnWin32View *flat = tree.box(10, 0, 0, 100, 100);
397
+ const float flattened[16] = {
398
+ 0, 0, 0, 0, //
399
+ 0, 0, 0, 0, //
400
+ 0, 0, 1, 0, //
401
+ 0, 0, 0, 1};
402
+ flat->setTransform(flattened);
403
+ root->insertChild(flat, 0);
404
+
405
+ float x = -1.0f;
406
+ float y = -1.0f;
407
+ EXPECT(!flat->pageToLocal(root, 10.0f, 10.0f, x, y));
408
+ // Untouched, so the caller's fallback is what is used.
409
+ EXPECT_NEAR(x, -1.0f, 0.001f);
410
+ }
@@ -0,0 +1,239 @@
1
+ // The resize modes, against a real bitmap.
2
+ //
3
+ // The same seven questions tests/test_appkit_image.mm asks of AppKit, and they
4
+ // are asked differently here. That suite checks the destination rect the view
5
+ // computed; this one renders and reads the pixels, because the arithmetic is
6
+ // only half the claim -- `cover` that scales correctly and forgets to clip
7
+ // looks right in a rect and wrong on a screen.
8
+ //
9
+ // The picture is a two-colour image: red on the left half, blue on the right.
10
+ // That makes it asymmetric, so a fit that mirrors or transposes is visible, and
11
+ // it makes "which part got cropped" answerable.
12
+
13
+ #include "TestHarness.h"
14
+
15
+ #include "RnWin32Image.h"
16
+ #include "RnWin32View.h"
17
+ #include "Win32Snapshot.h"
18
+
19
+ #include <cstdio>
20
+ #include <cstdlib>
21
+ #include <memory>
22
+ #include <sstream>
23
+ #include <vector>
24
+
25
+ using basalt::win32::RnImageFit;
26
+ using basalt::win32::RnPixel;
27
+ using basalt::win32::RnPixels;
28
+ using basalt::win32::RnWin32Image;
29
+ using basalt::win32::RnWin32View;
30
+
31
+ namespace {
32
+
33
+ constexpr double kTolerance = 3.0;
34
+
35
+ // Premultiplied BGRA, which is what fromPixels documents. Both halves are
36
+ // opaque, so premultiplied and straight are the same bytes here.
37
+ std::vector<uint8_t> twoTone(unsigned width, unsigned height) {
38
+ std::vector<uint8_t> pixels(static_cast<size_t>(width) * height * 4);
39
+ for (unsigned y = 0; y < height; y++) {
40
+ for (unsigned x = 0; x < width; x++) {
41
+ const size_t offset = (static_cast<size_t>(y) * width + x) * 4;
42
+ const bool leftHalf = x < width / 2;
43
+ pixels[offset + 0] = leftHalf ? 0 : 255; // blue
44
+ pixels[offset + 1] = 0; // green
45
+ pixels[offset + 2] = leftHalf ? 255 : 0; // red
46
+ pixels[offset + 3] = 255; // alpha
47
+ }
48
+ }
49
+ return pixels;
50
+ }
51
+
52
+ std::shared_ptr<RnWin32Image> twoToneImage(unsigned width, unsigned height) {
53
+ const std::vector<uint8_t> pixels = twoTone(width, height);
54
+ return RnWin32Image::fromPixels(width, height, pixels.data());
55
+ }
56
+
57
+ // Renders one view holding the image in a box, and hands back the pixels.
58
+ RnPixels renderInBox(const std::shared_ptr<RnWin32Image> &image,
59
+ RnImageFit fit,
60
+ float boxWidth,
61
+ float boxHeight) {
62
+ auto root = std::make_unique<RnWin32View>(1);
63
+ root->setFrame(0, 0, boxWidth, boxHeight);
64
+ root->setImage(image, fit);
65
+ return basalt::win32::renderToPixels(*root);
66
+ }
67
+
68
+ } // namespace
69
+
70
+ #define EXPECT_PIXEL(pixels, x, y, r, g, b, a) \
71
+ do { \
72
+ const RnPixel pixel = (pixels).at((x), (y)); \
73
+ EXPECT_NEAR(pixel.red, (r), kTolerance); \
74
+ EXPECT_NEAR(pixel.green, (g), kTolerance); \
75
+ EXPECT_NEAR(pixel.blue, (b), kTolerance); \
76
+ EXPECT_NEAR(pixel.alpha, (a), kTolerance); \
77
+ } while (false)
78
+
79
+ #define EXPECT_TRANSPARENT(pixels, x, y) EXPECT_NEAR((pixels).at((x), (y)).alpha, 0, kTolerance)
80
+
81
+ TEST(image_decodes_from_pixels) {
82
+ auto image = twoToneImage(40, 20);
83
+ EXPECT(image != nullptr);
84
+ EXPECT_EQ(image->width(), 40u);
85
+ EXPECT_EQ(image->height(), 20u);
86
+ }
87
+
88
+ TEST(image_stretch_fills_the_frame) {
89
+ auto image = twoToneImage(40, 20);
90
+ const RnPixels pixels = renderInBox(image, RnImageFit::Stretch, 200, 100);
91
+ EXPECT(!pixels.empty());
92
+
93
+ // Distorted to the whole box, so every corner is covered and the halves land
94
+ // where the aspect ratio says they should not.
95
+ EXPECT_PIXEL(pixels, 50, 50, 255, 0, 0, 255);
96
+ EXPECT_PIXEL(pixels, 150, 50, 0, 0, 255, 255);
97
+ EXPECT_PIXEL(pixels, 5, 5, 255, 0, 0, 255);
98
+ EXPECT_PIXEL(pixels, 195, 95, 0, 0, 255, 255);
99
+ }
100
+
101
+ TEST(image_contain_fits_inside_and_centres) {
102
+ // A 40x20 image in a 200x200 box: contain scales by 5, giving 200x100
103
+ // centred, so the top and bottom fifty rows are empty.
104
+ auto image = twoToneImage(40, 20);
105
+ const RnPixels pixels = renderInBox(image, RnImageFit::Contain, 200, 200);
106
+ EXPECT(!pixels.empty());
107
+
108
+ EXPECT_TRANSPARENT(pixels, 100, 10);
109
+ EXPECT_TRANSPARENT(pixels, 100, 190);
110
+ EXPECT_PIXEL(pixels, 50, 100, 255, 0, 0, 255);
111
+ EXPECT_PIXEL(pixels, 150, 100, 0, 0, 255, 255);
112
+ }
113
+
114
+ TEST(image_cover_fills_the_frame_and_crops) {
115
+ // The same image in the same box: cover scales by 10, giving 400x200, so the
116
+ // sides are cropped and nothing is empty.
117
+ auto image = twoToneImage(40, 20);
118
+ const RnPixels pixels = renderInBox(image, RnImageFit::Cover, 200, 200);
119
+ EXPECT(!pixels.empty());
120
+
121
+ // Top and bottom are covered, which is the difference from `contain` -- and
122
+ // sampled away from x=100, because the seam between the two halves lands
123
+ // exactly there and the sample would read the interpolation across it. That
124
+ // is not a nicety: the first version of this test asked at x=100 and got
125
+ // (115,0,140), which is a real answer to the wrong question.
126
+ EXPECT_PIXEL(pixels, 50, 10, 255, 0, 0, 255);
127
+ EXPECT_PIXEL(pixels, 50, 190, 255, 0, 0, 255);
128
+ // The seam is at the image's centre, which cover keeps at the box's centre --
129
+ // so the left of the box is red and the right is blue, with the outer
130
+ // quarters of the image cropped away.
131
+ EXPECT_PIXEL(pixels, 20, 100, 255, 0, 0, 255);
132
+ EXPECT_PIXEL(pixels, 180, 100, 0, 0, 255, 255);
133
+ }
134
+
135
+ TEST(image_cover_does_not_paint_outside_the_frame) {
136
+ // The clip is the half of `cover` a destination rect cannot show. The image
137
+ // is drawn 400 wide inside a 200-wide view, so without the clip it would
138
+ // reach the surrounding root.
139
+ auto image = twoToneImage(40, 20);
140
+
141
+ auto root = std::make_unique<RnWin32View>(1);
142
+ root->setFrame(0, 0, 400, 200);
143
+ auto framed = std::make_unique<RnWin32View>(2);
144
+ framed->setFrame(100, 0, 200, 200);
145
+ framed->setImage(image, RnImageFit::Cover);
146
+ root->insertChild(framed.get(), 0);
147
+
148
+ const RnPixels pixels = basalt::win32::renderToPixels(*root);
149
+ EXPECT(!pixels.empty());
150
+
151
+ EXPECT_PIXEL(pixels, 150, 100, 255, 0, 0, 255);
152
+ EXPECT_TRANSPARENT(pixels, 50, 100);
153
+ EXPECT_TRANSPARENT(pixels, 350, 100);
154
+ }
155
+
156
+ TEST(image_center_draws_at_natural_size) {
157
+ // 40x20 in a 200x200 box, centred and unscaled: it occupies x 80..120,
158
+ // y 90..110.
159
+ auto image = twoToneImage(40, 20);
160
+ const RnPixels pixels = renderInBox(image, RnImageFit::Center, 200, 200);
161
+ EXPECT(!pixels.empty());
162
+
163
+ EXPECT_PIXEL(pixels, 90, 100, 255, 0, 0, 255);
164
+ EXPECT_PIXEL(pixels, 110, 100, 0, 0, 255, 255);
165
+ EXPECT_TRANSPARENT(pixels, 60, 100);
166
+ EXPECT_TRANSPARENT(pixels, 100, 70);
167
+ }
168
+
169
+ TEST(image_center_shrinks_an_oversized_image) {
170
+ // Bigger than the box, so `center` behaves like `contain` rather than
171
+ // overflowing: 400x200 into 200x200 scales by a half to 200x100, centred.
172
+ auto image = twoToneImage(400, 200);
173
+ const RnPixels pixels = renderInBox(image, RnImageFit::Center, 200, 200);
174
+ EXPECT(!pixels.empty());
175
+
176
+ EXPECT_PIXEL(pixels, 50, 100, 255, 0, 0, 255);
177
+ EXPECT_PIXEL(pixels, 150, 100, 0, 0, 255, 255);
178
+ EXPECT_TRANSPARENT(pixels, 100, 10);
179
+ }
180
+
181
+ TEST(image_is_reported_in_the_tree) {
182
+ auto view = std::make_unique<RnWin32View>(9);
183
+ view->setFrame(0, 0, 64, 64);
184
+ view->setImage(twoToneImage(40, 20), RnImageFit::Contain);
185
+
186
+ EXPECT_EQ(view->describeTree(),
187
+ std::string("view tag=9 frame=(0,0 64x64) texture=40x20 fit=contain\n"));
188
+ }
189
+
190
+ TEST(image_can_be_cleared) {
191
+ auto view = std::make_unique<RnWin32View>(9);
192
+ view->setFrame(0, 0, 64, 64);
193
+ view->setImage(twoToneImage(40, 20), RnImageFit::Cover);
194
+ view->setImage(nullptr, RnImageFit::Cover);
195
+
196
+ EXPECT(view->image() == nullptr);
197
+ EXPECT_EQ(view->describeTree(), std::string("view tag=9 frame=(0,0 64x64)\n"));
198
+ }
199
+
200
+ TEST(image_decodes_an_encoded_png) {
201
+ // A round trip through the two halves of this platform's image handling: the
202
+ // snapshot encoder writes a PNG, and WIC reads it back. Cheaper than checking
203
+ // in a fixture, and it fails if either half breaks.
204
+ auto source = std::make_unique<RnWin32View>(1);
205
+ source->setFrame(0, 0, 24, 12);
206
+ source->setBackgroundColor(0.0f, 1.0f, 0.0f, 1.0f, true);
207
+
208
+ const std::string path = std::string(std::getenv("TEMP") ? std::getenv("TEMP") : ".") +
209
+ "\\basalt_win32_roundtrip.png";
210
+ EXPECT(basalt::win32::writeSnapshot(*source, path));
211
+
212
+ std::vector<uint8_t> encoded;
213
+ if (FILE *file = std::fopen(path.c_str(), "rb")) {
214
+ std::fseek(file, 0, SEEK_END);
215
+ const long size = std::ftell(file);
216
+ std::fseek(file, 0, SEEK_SET);
217
+ encoded.resize(static_cast<size_t>(size > 0 ? size : 0));
218
+ if (!encoded.empty()) {
219
+ const size_t read = std::fread(encoded.data(), 1, encoded.size(), file);
220
+ encoded.resize(read);
221
+ }
222
+ std::fclose(file);
223
+ }
224
+ EXPECT(!encoded.empty());
225
+
226
+ auto decoded = RnWin32Image::fromEncodedBytes(encoded.data(), encoded.size());
227
+ EXPECT(decoded != nullptr);
228
+ if (decoded != nullptr) {
229
+ EXPECT_EQ(decoded->width(), 24u);
230
+ EXPECT_EQ(decoded->height(), 12u);
231
+ }
232
+ std::remove(path.c_str());
233
+ }
234
+
235
+ TEST(image_rejects_bytes_that_are_not_an_image) {
236
+ const uint8_t rubbish[] = {'n', 'o', 't', ' ', 'a', ' ', 'p', 'n', 'g'};
237
+ EXPECT(RnWin32Image::fromEncodedBytes(rubbish, sizeof(rubbish)) == nullptr);
238
+ EXPECT(RnWin32Image::fromEncodedBytes(nullptr, 0) == nullptr);
239
+ }