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,385 @@
1
+ // Tests for <ScrollView> on Windows: the offset, the clamp, the wheel's
2
+ // routing, and the commands.
3
+ //
4
+ // The first tests any of the three desktops has for a scroll manager. GTK's and
5
+ // AppKit's are exercised end to end by `scripts/compare_hosts.sh` running
6
+ // `js/scroll.js` on both, which is a better test in every way except that it
7
+ // needs two working desktops and this machine has one -- WSL2 will not start
8
+ // here, so nothing else can check Windows against another host. So the
9
+ // arithmetic gets tested directly instead.
10
+ //
11
+ // What is observable from here is narrower than what the manager does, and the
12
+ // two limits are worth naming rather than working around. A ShadowView built by
13
+ // hand has no ShadowNodeFamily, so `State::updateState` finds nothing to commit
14
+ // into and the unthrottled state write-back cannot be asserted on. And an
15
+ // EventEmitter built by hand has no EventDispatcher, so `onScroll` goes
16
+ // nowhere. What is left is the part that decides where the content actually
17
+ // sits: the clamp, the adoption, the routing and the commands.
18
+
19
+ #include "TestHarness.h"
20
+
21
+ #include "Win32MountingManager.h"
22
+ #include "Win32ScrollView.h"
23
+ // For hitTestTag: what a press would find, which is the other half of "the
24
+ // scroll moved" and the half a scroll manager can get silently wrong.
25
+ #include "Win32TouchDispatcher.h"
26
+
27
+ #include <react/renderer/components/scrollview/ScrollViewProps.h>
28
+ #include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
29
+ #include <react/renderer/components/scrollview/ScrollViewState.h>
30
+ #include <react/renderer/components/view/ViewProps.h>
31
+
32
+ #include <memory>
33
+
34
+ using basalt::Win32MountingManager;
35
+ using basalt::Win32ScrollViewManager;
36
+ using basalt::win32::RnWin32View;
37
+ using facebook::react::LayoutMetrics;
38
+ using facebook::react::MountingTransaction;
39
+ using facebook::react::Point;
40
+ using facebook::react::Rect;
41
+ using facebook::react::ScrollViewProps;
42
+ using facebook::react::ScrollViewShadowNode;
43
+ using facebook::react::ScrollViewState;
44
+ using facebook::react::ShadowNodeFamily;
45
+ using facebook::react::ShadowView;
46
+ using facebook::react::ShadowViewMutation;
47
+ using facebook::react::ShadowViewMutationList;
48
+ using facebook::react::Size;
49
+ using facebook::react::SurfaceId;
50
+ using facebook::react::Tag;
51
+ using facebook::react::TransactionTelemetry;
52
+ using facebook::react::ViewProps;
53
+
54
+ namespace {
55
+
56
+ constexpr SurfaceId kSurfaceId = 1;
57
+
58
+ ShadowView makeView(Tag tag, float x, float y, float width, float height) {
59
+ LayoutMetrics metrics;
60
+ metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
61
+
62
+ ShadowView view;
63
+ view.componentName = "View";
64
+ view.surfaceId = kSurfaceId;
65
+ view.tag = tag;
66
+ view.props = std::make_shared<ViewProps>();
67
+ view.layoutMetrics = metrics;
68
+ return view;
69
+ }
70
+
71
+ // A ScrollView whose viewport is `width`x`height` and whose content is
72
+ // `contentWidth`x`contentHeight` -- which is the whole of what the manager
73
+ // needs, because Yoga has already laid the content out at its full size by the
74
+ // time a mutation arrives.
75
+ ShadowView makeScrollView(Tag tag,
76
+ float x,
77
+ float y,
78
+ float width,
79
+ float height,
80
+ float contentWidth,
81
+ float contentHeight,
82
+ bool scrollEnabled = true,
83
+ Point initialOffset = Point{0, 0}) {
84
+ LayoutMetrics metrics;
85
+ metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
86
+
87
+ auto props = std::make_shared<ScrollViewProps>();
88
+ props->scrollEnabled = scrollEnabled;
89
+
90
+ ScrollViewState data;
91
+ data.contentOffset = initialOffset;
92
+ data.contentBoundingRect = Rect{.origin = {.x = 0, .y = 0},
93
+ .size = Size{.width = contentWidth, .height = contentHeight}};
94
+
95
+ ShadowView view;
96
+ view.componentName = "ScrollView";
97
+ view.surfaceId = kSurfaceId;
98
+ view.tag = tag;
99
+ view.props = props;
100
+ view.layoutMetrics = metrics;
101
+ // An empty family: nothing can be committed back through it, which is what
102
+ // makes the state write-back unobservable here. See the file header.
103
+ view.state = std::make_shared<const ScrollViewShadowNode::ConcreteState>(
104
+ std::make_shared<const ScrollViewState>(data), ShadowNodeFamily::Weak{});
105
+ return view;
106
+ }
107
+
108
+ void apply(Win32MountingManager &manager, ShadowViewMutationList &&mutations) {
109
+ manager.applyTransaction(
110
+ kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
111
+ }
112
+
113
+ // Creates and inserts one shadow view under `parent`, in a single transaction.
114
+ void mount(Win32MountingManager &manager, Tag parent, const ShadowView &shadowView) {
115
+ ShadowViewMutationList mutations;
116
+ mutations.push_back(ShadowViewMutation::CreateMutation(shadowView));
117
+ mutations.push_back(ShadowViewMutation::InsertMutation(parent, shadowView, 0));
118
+ apply(manager, std::move(mutations));
119
+ }
120
+
121
+ // One notch's worth of pixels, in the direction a contentOffset grows.
122
+ constexpr double kNotch = Win32ScrollViewManager::kWheelStepPixels;
123
+
124
+ } // namespace
125
+
126
+ // The offset is applied to the view, which is where painting and hit testing
127
+ // both read it -- so this is the one assertion that covers all three.
128
+ TEST(win32_a_wheel_scrolls_the_view_under_it) {
129
+ Win32MountingManager manager;
130
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
131
+ root->setFrame(0, 0, 400, 300);
132
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
133
+
134
+ EXPECT(manager.scrollAt(root, 200, 150, 0, 5 * kNotch));
135
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 5 * kNotch, 0.01);
136
+ EXPECT_NEAR(manager.viewForTag(10)->scrollX(), 0.0, 0.01);
137
+
138
+ manager.destroySurfaceRoot(kSurfaceId);
139
+ }
140
+
141
+ // The clamp, at both ends. Content 1000 tall in a 300 viewport can scroll 700
142
+ // and no further, and nothing can scroll above the top.
143
+ TEST(win32_the_offset_is_clamped_to_the_content) {
144
+ Win32MountingManager manager;
145
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
146
+ root->setFrame(0, 0, 400, 300);
147
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
148
+
149
+ manager.scrollAt(root, 200, 150, 0, 100 * kNotch);
150
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
151
+
152
+ manager.scrollAt(root, 200, 150, 0, -100 * kNotch);
153
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
154
+
155
+ manager.destroySurfaceRoot(kSurfaceId);
156
+ }
157
+
158
+ // Content that fits does not scroll at all, but the wheel is still consumed --
159
+ // matching AppKit, and matching what every desktop does: a list at its end must
160
+ // not hand the wheel to whatever is behind it.
161
+ TEST(win32_content_that_fits_consumes_the_wheel_and_does_not_move) {
162
+ Win32MountingManager manager;
163
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
164
+ root->setFrame(0, 0, 400, 300);
165
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 200));
166
+
167
+ EXPECT(manager.scrollAt(root, 200, 150, 0, 5 * kNotch));
168
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
169
+
170
+ manager.destroySurfaceRoot(kSurfaceId);
171
+ }
172
+
173
+ // scrollEnabled: false does not consume, so the wheel keeps going up the tree.
174
+ TEST(win32_a_disabled_scroll_view_passes_the_wheel_on) {
175
+ Win32MountingManager manager;
176
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
177
+ root->setFrame(0, 0, 400, 300);
178
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000, false));
179
+
180
+ EXPECT(!manager.scrollAt(root, 200, 150, 0, 5 * kNotch));
181
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
182
+
183
+ manager.destroySurfaceRoot(kSurfaceId);
184
+ }
185
+
186
+ // A wheel over a plain row scrolls the list containing it. GTK gets this from
187
+ // attaching a controller per widget and AppKit from the responder chain; here
188
+ // it is the parent walk in scrollAt, so it is worth an assertion.
189
+ TEST(win32_a_wheel_over_a_child_scrolls_its_scroll_view) {
190
+ Win32MountingManager manager;
191
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
192
+ root->setFrame(0, 0, 400, 300);
193
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
194
+ mount(manager, 10, makeView(11, 0, 0, 400, 80));
195
+
196
+ // The point lands on the row, not on the ScrollView.
197
+ EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 200, 40)), 11L);
198
+
199
+ EXPECT(manager.scrollAt(root, 200, 40, 0, 2 * kNotch));
200
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 2 * kNotch, 0.01);
201
+
202
+ manager.destroySurfaceRoot(kSurfaceId);
203
+ }
204
+
205
+ // A list inside a list scrolls the inner one. Innermost-first is the whole
206
+ // reason the walk starts at the hit view rather than at the root.
207
+ TEST(win32_a_nested_scroll_view_takes_the_wheel_first) {
208
+ Win32MountingManager manager;
209
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
210
+ root->setFrame(0, 0, 400, 300);
211
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
212
+ mount(manager, 10, makeScrollView(11, 0, 0, 400, 100, 400, 500));
213
+
214
+ EXPECT(manager.scrollAt(root, 200, 50, 0, 1 * kNotch));
215
+ EXPECT_NEAR(manager.viewForTag(11)->scrollY(), kNotch, 0.01);
216
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
217
+
218
+ manager.destroySurfaceRoot(kSurfaceId);
219
+ }
220
+
221
+ // Scrolling moves what a press lands on. `hitTest` adds the scroll offset back
222
+ // on the way down, so this needs nothing from the scroll manager -- which is
223
+ // exactly the claim being checked, because a list that scrolls correctly and
224
+ // answers presses meant for the row above is the bug this prevents.
225
+ TEST(win32_hit_testing_follows_the_scroll) {
226
+ Win32MountingManager manager;
227
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
228
+ root->setFrame(0, 0, 400, 300);
229
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
230
+ // Two rows, the second below the fold at y=400.
231
+ mount(manager, 10, makeView(12, 0, 400, 400, 100));
232
+ mount(manager, 10, makeView(11, 0, 0, 400, 100));
233
+
234
+ EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 200, 50)), 11L);
235
+
236
+ manager.scrollAt(root, 200, 150, 0, 400);
237
+ EXPECT_EQ(static_cast<long>(basalt::hitTestTag(root, 200, 50)), 12L);
238
+
239
+ manager.destroySurfaceRoot(kSurfaceId);
240
+ }
241
+
242
+ // A ScrollView must clip whatever its props say. Its content is deliberately
243
+ // larger than its frame, and without this it paints over its siblings.
244
+ TEST(win32_a_scroll_view_clips_regardless_of_overflow) {
245
+ Win32MountingManager manager;
246
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
247
+ root->setFrame(0, 0, 400, 300);
248
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
249
+
250
+ EXPECT(manager.viewForTag(10)->clipsChildren());
251
+
252
+ manager.destroySurfaceRoot(kSurfaceId);
253
+ }
254
+
255
+ // An offset already in the state is adopted rather than reset. That is what a
256
+ // `contentOffset` prop looks like by the time it reaches here, and what a
257
+ // surface that was suspended and remounted carries.
258
+ TEST(win32_an_initial_offset_in_the_state_is_adopted) {
259
+ Win32MountingManager manager;
260
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
261
+ root->setFrame(0, 0, 400, 300);
262
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000, true, Point{0, 120}));
263
+
264
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 120.0, 0.01);
265
+
266
+ manager.destroySurfaceRoot(kSurfaceId);
267
+ }
268
+
269
+ // Content can shrink under a scrolled offset -- a list whose items were
270
+ // filtered away. The re-clamp on every mutation is what stops the view from
271
+ // being left showing empty space below its own content.
272
+ TEST(win32_shrinking_content_pulls_the_offset_back) {
273
+ Win32MountingManager manager;
274
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
275
+ root->setFrame(0, 0, 400, 300);
276
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
277
+
278
+ manager.scrollAt(root, 200, 150, 0, 700);
279
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
280
+
281
+ ShadowViewMutationList mutations;
282
+ mutations.push_back(
283
+ ShadowViewMutation::UpdateMutation(makeScrollView(10, 0, 0, 400, 300, 400, 1000),
284
+ makeScrollView(10, 0, 0, 400, 300, 400, 500),
285
+ kSurfaceId));
286
+ apply(manager, std::move(mutations));
287
+
288
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 200.0, 0.01);
289
+
290
+ manager.destroySurfaceRoot(kSurfaceId);
291
+ }
292
+
293
+ // scrollTo and scrollToEnd, which arrive through dispatchCommand. js/scroll.js
294
+ // drives itself through a ref, so this is the path that file exercises and the
295
+ // only one that needs no wheel.
296
+ TEST(win32_scroll_to_and_scroll_to_end_move_the_offset) {
297
+ Win32MountingManager manager;
298
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
299
+ root->setFrame(0, 0, 400, 300);
300
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
301
+
302
+ manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 265, false));
303
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 265.0, 0.01);
304
+
305
+ manager.applyCommand(10, "scrollToEnd", folly::dynamic::array(false));
306
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
307
+
308
+ // scrollTo past the end is clamped like any other offset.
309
+ manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 5000, false));
310
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 700.0, 0.01);
311
+
312
+ manager.destroySurfaceRoot(kSurfaceId);
313
+ }
314
+
315
+ // A command for a tag that is not a ScrollView, and one for a tag that is gone.
316
+ // Both are ordinary: a scrollTo can arrive after the surface that owned it was
317
+ // torn down.
318
+ TEST(win32_a_command_for_an_unknown_tag_is_survivable) {
319
+ Win32MountingManager manager;
320
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
321
+ root->setFrame(0, 0, 400, 300);
322
+ mount(manager, kSurfaceId, makeView(10, 0, 0, 100, 100));
323
+
324
+ manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 100, false));
325
+ manager.applyCommand(999, "scrollTo", folly::dynamic::array(0, 100, false));
326
+ manager.applyCommand(10, "somethingElse", folly::dynamic::array());
327
+
328
+ manager.destroySurfaceRoot(kSurfaceId);
329
+ }
330
+
331
+ // The entry has to go with the view. A wheel arriving after the ScrollView was
332
+ // deleted must find nothing rather than a dangling RnWin32View pointer.
333
+ TEST(win32_deleting_a_scroll_view_forgets_its_entry) {
334
+ Win32MountingManager manager;
335
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
336
+ root->setFrame(0, 0, 400, 300);
337
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000));
338
+ EXPECT(manager.scrollAt(root, 200, 150, 0, kNotch));
339
+
340
+ ShadowViewMutationList mutations;
341
+ mutations.push_back(ShadowViewMutation::RemoveMutation(
342
+ kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 400, 1000), 0));
343
+ mutations.push_back(
344
+ ShadowViewMutation::DeleteMutation(makeScrollView(10, 0, 0, 400, 300, 400, 1000)));
345
+ apply(manager, std::move(mutations));
346
+
347
+ EXPECT(manager.viewForTag(10) == nullptr);
348
+ EXPECT(!manager.scrollAt(root, 200, 150, 0, kNotch));
349
+ manager.applyCommand(10, "scrollTo", folly::dynamic::array(0, 100, false));
350
+
351
+ manager.destroySurfaceRoot(kSurfaceId);
352
+ }
353
+
354
+ // Nothing under the pointer, and no surface at all. The host reads the return
355
+ // value to decide whether to leave the message to DefWindowProc, so "false"
356
+ // here is the difference between a wheel over a non-scrolling app doing nothing
357
+ // and doing nothing *and* being swallowed.
358
+ TEST(win32_a_wheel_over_nothing_is_not_consumed) {
359
+ Win32MountingManager manager;
360
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
361
+ root->setFrame(0, 0, 400, 300);
362
+ mount(manager, kSurfaceId, makeView(10, 0, 0, 100, 100));
363
+
364
+ EXPECT(!manager.scrollAt(root, 50, 50, 0, kNotch));
365
+ EXPECT(!manager.scrollAt(root, 5000, 5000, 0, kNotch));
366
+ EXPECT(!manager.scrollAt(nullptr, 50, 50, 0, kNotch));
367
+
368
+ manager.destroySurfaceRoot(kSurfaceId);
369
+ }
370
+
371
+ // Horizontal, which travels the same path with the other axis and is easy to
372
+ // leave half-wired: WM_MOUSEHWHEEL is a separate message with the opposite
373
+ // sign convention to WM_MOUSEWHEEL.
374
+ TEST(win32_a_horizontal_wheel_scrolls_the_other_axis) {
375
+ Win32MountingManager manager;
376
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
377
+ root->setFrame(0, 0, 400, 300);
378
+ mount(manager, kSurfaceId, makeScrollView(10, 0, 0, 400, 300, 1200, 300));
379
+
380
+ EXPECT(manager.scrollAt(root, 200, 150, 3 * kNotch, 0));
381
+ EXPECT_NEAR(manager.viewForTag(10)->scrollX(), 3 * kNotch, 0.01);
382
+ EXPECT_NEAR(manager.viewForTag(10)->scrollY(), 0.0, 0.01);
383
+
384
+ manager.destroySurfaceRoot(kSurfaceId);
385
+ }
@@ -0,0 +1,258 @@
1
+ // DirectWrite measurement, and where the glyphs land.
2
+ //
3
+ // The union of what tests/test_text.cpp asks of Pango and
4
+ // tests/test_appkit_text.mm asks of Core Text. Two of them are regression tests
5
+ // on the other platforms rather than general questions, and they are here
6
+ // because the bug each describes is a bug any text engine can have:
7
+ // `font_size_is_absolute_not_points`, and that a default ellipsize mode does
8
+ // not collapse a wrapping paragraph.
9
+ //
10
+ // No React Native and no window: RnWin32TextLayout knows nothing about either.
11
+
12
+ #include "TestHarness.h"
13
+
14
+ #include "RnWin32TextLayout.h"
15
+ #include "RnWin32View.h"
16
+ #include "Win32Snapshot.h"
17
+
18
+ #include <memory>
19
+ #include <sstream>
20
+ #include <string>
21
+
22
+ using basalt::win32::RnTextAlign;
23
+ using basalt::win32::RnTextSize;
24
+ using basalt::win32::RnTextStyle;
25
+ using basalt::win32::RnWin32TextLayout;
26
+ using basalt::win32::RnWin32View;
27
+
28
+ namespace {
29
+
30
+ std::shared_ptr<RnWin32TextLayout> paragraph(const std::string &text,
31
+ float fontSize = 16.0f,
32
+ int maxLines = 0) {
33
+ RnTextStyle style;
34
+ style.fontSize = fontSize;
35
+ return RnWin32TextLayout::create(text, style, maxLines);
36
+ }
37
+
38
+ constexpr const char *kLongText =
39
+ "The quick brown fox jumps over the lazy dog, and then does it again "
40
+ "because once was never going to be enough for a wrapping test.";
41
+
42
+ } // namespace
43
+
44
+ TEST(text_has_a_size) {
45
+ auto layout = paragraph("Hello");
46
+ EXPECT(layout != nullptr);
47
+
48
+ const RnTextSize size = layout->measure(-1.0f);
49
+ EXPECT(size.width > 0.0f);
50
+ EXPECT(size.height > 0.0f);
51
+ }
52
+
53
+ TEST(empty_text_measures_to_zero_width) {
54
+ auto layout = paragraph("");
55
+ EXPECT(layout != nullptr);
56
+
57
+ const RnTextSize size = layout->measure(-1.0f);
58
+ EXPECT_NEAR(size.width, 0.0, 0.5);
59
+ // A height, though: an empty line still occupies one. Both other platforms
60
+ // report the same, and a zero-height empty <Text> would collapse a layout
61
+ // that was relying on it to hold a row open.
62
+ EXPECT(size.height > 0.0f);
63
+ }
64
+
65
+ TEST(larger_font_measures_larger) {
66
+ const RnTextSize small = paragraph("Hello", 12.0f)->measure(-1.0f);
67
+ const RnTextSize large = paragraph("Hello", 36.0f)->measure(-1.0f);
68
+
69
+ EXPECT(large.width > small.width);
70
+ EXPECT(large.height > small.height);
71
+ }
72
+
73
+ TEST(font_size_is_absolute_not_points) {
74
+ // The bug this guards against, in the shape Pango had it: a size interpreted
75
+ // as points and resolved against 96dpi renders about a third larger than
76
+ // asked. React Native's fontSize is in density-independent pixels and every
77
+ // coordinate here lives in that same space, so a hundred-unit font must
78
+ // produce a line in the neighbourhood of a hundred units -- not a hundred and
79
+ // thirty-three, and certainly not a hundred and seventy-seven.
80
+ //
81
+ // The bounds are wide because a font's natural line spacing is its own
82
+ // business; what they exclude is a multiply by 96/72.
83
+ const RnTextSize size = paragraph("Hg", 100.0f)->measure(-1.0f);
84
+ EXPECT(size.height >= 90.0f);
85
+ EXPECT(size.height < 160.0f);
86
+ }
87
+
88
+ TEST(constraining_the_width_wraps_and_grows_taller) {
89
+ auto layout = paragraph(kLongText);
90
+
91
+ const RnTextSize wide = layout->measure(-1.0f);
92
+ const RnTextSize narrow = layout->measure(200.0f);
93
+
94
+ EXPECT(narrow.width <= 200.5f);
95
+ EXPECT(narrow.width < wide.width);
96
+ EXPECT(narrow.height > wide.height);
97
+ }
98
+
99
+ TEST(number_of_lines_truncates) {
100
+ auto unlimited = paragraph(kLongText, 16.0f, 0);
101
+ auto limited = paragraph(kLongText, 16.0f, 2);
102
+
103
+ const RnTextSize full = unlimited->measure(200.0f);
104
+ const RnTextSize clipped = limited->measure(200.0f);
105
+
106
+ EXPECT(clipped.height < full.height);
107
+ EXPECT(clipped.height > 0.0f);
108
+ }
109
+
110
+ TEST(a_line_limit_that_fits_is_not_truncated) {
111
+ // Five lines allowed and the paragraph needs fewer, so the limit must change
112
+ // nothing at all -- not the height, and not the text.
113
+ auto unlimited = paragraph("Short", 16.0f, 0);
114
+ auto limited = paragraph("Short", 16.0f, 5);
115
+
116
+ EXPECT_NEAR(limited->measure(-1.0f).height, unlimited->measure(-1.0f).height, 0.01);
117
+ EXPECT_NEAR(limited->measure(-1.0f).width, unlimited->measure(-1.0f).width, 0.01);
118
+ }
119
+
120
+ TEST(default_ellipsize_mode_does_not_collapse_a_paragraph) {
121
+ // Pango's trap, stated as a test: React Native's ellipsizeMode defaults to
122
+ // tail, and translating that faithfully by switching ellipsization on without
123
+ // also setting a height collapsed every wrapping paragraph to one line. A
124
+ // paragraph with no line limit must wrap to as many lines as it needs.
125
+ auto layout = paragraph(kLongText);
126
+
127
+ const RnTextSize oneLine = paragraph("Hello")->measure(-1.0f);
128
+ const RnTextSize wrapped = layout->measure(200.0f);
129
+
130
+ EXPECT(wrapped.height > oneLine.height * 2.0f);
131
+ }
132
+
133
+ TEST(bold_is_wider_than_regular) {
134
+ RnTextStyle regular;
135
+ regular.fontSize = 24.0f;
136
+ RnTextStyle bold = regular;
137
+ bold.bold = true;
138
+
139
+ const RnTextSize plain =
140
+ RnWin32TextLayout::create("Handgloves", regular, 0)->measure(-1.0f);
141
+ const RnTextSize heavy = RnWin32TextLayout::create("Handgloves", bold, 0)->measure(-1.0f);
142
+
143
+ EXPECT(heavy.width > plain.width);
144
+ }
145
+
146
+ TEST(alignment_does_not_change_the_measured_size) {
147
+ RnTextStyle left;
148
+ left.fontSize = 16.0f;
149
+ RnTextStyle centred = left;
150
+ centred.align = RnTextAlign::Center;
151
+
152
+ const RnTextSize a = RnWin32TextLayout::create(kLongText, left, 0)->measure(300.0f);
153
+ const RnTextSize b = RnWin32TextLayout::create(kLongText, centred, 0)->measure(300.0f);
154
+
155
+ // Alignment moves glyphs inside the box. It does not change how big the box
156
+ // has to be, and a platform that reported otherwise would make Yoga lay out
157
+ // a centred paragraph differently from a left-aligned one.
158
+ EXPECT_NEAR(a.width, b.width, 0.01);
159
+ EXPECT_NEAR(a.height, b.height, 0.01);
160
+ }
161
+
162
+ TEST(text_is_reported_in_the_tree) {
163
+ auto view = std::make_unique<RnWin32View>(7);
164
+ view->setFrame(0, 0, 200, 40);
165
+ view->setTextLayout(paragraph("Hi \"there\"\nyou"));
166
+
167
+ // Escaped the way the GTK side escapes it, so a quote or a newline keeps the
168
+ // dump to one line per view.
169
+ EXPECT_EQ(view->describeTree(),
170
+ std::string("view tag=7 frame=(0,0 200x40) text=\"Hi \\\"there\\\"\\nyou\"\n"));
171
+ }
172
+
173
+ TEST(text_draws_where_the_alignment_says) {
174
+ // The assertion the other two hosts cannot make: not that alignment was set,
175
+ // but that the glyphs moved. A left-aligned line puts ink near the left edge
176
+ // and none near the right; a right-aligned one does the opposite.
177
+ const auto inkColumns = [](RnTextAlign align) {
178
+ RnTextStyle style;
179
+ style.fontSize = 24.0f;
180
+ style.align = align;
181
+
182
+ auto root = std::make_unique<RnWin32View>(1);
183
+ root->setFrame(0, 0, 300, 40);
184
+ root->setTextLayout(RnWin32TextLayout::create("Ill", style, 0));
185
+
186
+ const basalt::win32::RnPixels pixels = basalt::win32::renderToPixels(*root);
187
+ int left = 0;
188
+ int right = 0;
189
+ for (unsigned y = 0; y < pixels.height(); y++) {
190
+ for (unsigned x = 0; x < 60; x++) {
191
+ if (pixels.at(x, y).alpha > 32) {
192
+ left++;
193
+ }
194
+ if (pixels.at(pixels.width() - 1 - x, y).alpha > 32) {
195
+ right++;
196
+ }
197
+ }
198
+ }
199
+ return std::pair<int, int>{left, right};
200
+ };
201
+
202
+ const auto [leftInkWhenLeft, rightInkWhenLeft] = inkColumns(RnTextAlign::Left);
203
+ EXPECT(leftInkWhenLeft > 0);
204
+ EXPECT_EQ(rightInkWhenLeft, 0);
205
+
206
+ const auto [leftInkWhenRight, rightInkWhenRight] = inkColumns(RnTextAlign::Right);
207
+ EXPECT_EQ(leftInkWhenRight, 0);
208
+ EXPECT(rightInkWhenRight > 0);
209
+ }
210
+
211
+ TEST(each_run_draws_in_its_own_colour) {
212
+ // `Hello <Text style={{color:'red'}}>world</Text>` is two runs of one
213
+ // paragraph, and DirectWrite carries colour as a *drawing effect* rather than
214
+ // as a range attribute -- so a layout built the obvious way renders the whole
215
+ // paragraph in the first run's colour. Both other desktops get this from
216
+ // their attributed string for free, which is why the gap here lasted as long
217
+ // as it did.
218
+ //
219
+ // Ink rather than an exact pixel: where a glyph lands depends on the font,
220
+ // and what is being asked is only which colours appear at all.
221
+ RnTextStyle red;
222
+ red.fontSize = 32.0f;
223
+ red.color[0] = 1.0f;
224
+ red.color[1] = 0.0f;
225
+ red.color[2] = 0.0f;
226
+
227
+ RnTextStyle blue = red;
228
+ blue.color[0] = 0.0f;
229
+ blue.color[2] = 1.0f;
230
+
231
+ auto root = std::make_unique<RnWin32View>(1);
232
+ root->setFrame(0, 0, 400, 60);
233
+ root->setTextLayout(RnWin32TextLayout::createFromRuns(
234
+ {basalt::win32::RnTextRun{"IIII", red}, basalt::win32::RnTextRun{"IIII", blue}}, 0));
235
+
236
+ const basalt::win32::RnPixels pixels = basalt::win32::renderToPixels(*root);
237
+ EXPECT(!pixels.empty());
238
+
239
+ int reddish = 0;
240
+ int bluish = 0;
241
+ for (unsigned y = 0; y < pixels.height(); y++) {
242
+ for (unsigned x = 0; x < pixels.width(); x++) {
243
+ const auto pixel = pixels.at(x, y);
244
+ if (pixel.alpha < 128) {
245
+ continue;
246
+ }
247
+ if (pixel.red > pixel.blue + 64) {
248
+ reddish++;
249
+ }
250
+ if (pixel.blue > pixel.red + 64) {
251
+ bluish++;
252
+ }
253
+ }
254
+ }
255
+
256
+ EXPECT(reddish > 0);
257
+ EXPECT(bluish > 0);
258
+ }