basalt-gtk 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 (71) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runLinux.js +29 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +362 -0
  5. package/native/gtk/ColorSchemeGtk.cpp +58 -0
  6. package/native/gtk/ComponentRegistryGtk.cpp +101 -0
  7. package/native/gtk/FontRegistryFontconfig.cpp +127 -0
  8. package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
  9. package/native/gtk/GtkAnimationChoreographer.h +49 -0
  10. package/native/gtk/GtkDropTarget.cpp +269 -0
  11. package/native/gtk/GtkDropTarget.h +40 -0
  12. package/native/gtk/GtkFocus.cpp +296 -0
  13. package/native/gtk/GtkFocus.h +113 -0
  14. package/native/gtk/GtkImageLoader.cpp +128 -0
  15. package/native/gtk/GtkImageLoader.h +87 -0
  16. package/native/gtk/GtkKeyEvents.cpp +91 -0
  17. package/native/gtk/GtkKeyEvents.h +21 -0
  18. package/native/gtk/GtkMenuBar.cpp +45 -0
  19. package/native/gtk/GtkMountingManager.cpp +1027 -0
  20. package/native/gtk/GtkMountingManager.h +146 -0
  21. package/native/gtk/GtkRunLoopObserver.cpp +77 -0
  22. package/native/gtk/GtkRunLoopObserver.h +41 -0
  23. package/native/gtk/GtkScrollView.cpp +626 -0
  24. package/native/gtk/GtkScrollView.h +209 -0
  25. package/native/gtk/GtkSkiaContext.cpp +248 -0
  26. package/native/gtk/GtkSkiaContext.h +62 -0
  27. package/native/gtk/GtkSkiaModule.cpp +80 -0
  28. package/native/gtk/GtkSkiaModule.h +51 -0
  29. package/native/gtk/GtkSkiaPeer.cpp +285 -0
  30. package/native/gtk/GtkSkiaPeer.h +63 -0
  31. package/native/gtk/GtkTextInput.cpp +428 -0
  32. package/native/gtk/GtkTextInput.h +123 -0
  33. package/native/gtk/GtkTextPeer.cpp +425 -0
  34. package/native/gtk/GtkTextPeer.h +87 -0
  35. package/native/gtk/GtkTitleBar.cpp +273 -0
  36. package/native/gtk/GtkTitleBar.h +96 -0
  37. package/native/gtk/GtkTitleBarLayout.cpp +45 -0
  38. package/native/gtk/GtkTitleBarLayout.h +36 -0
  39. package/native/gtk/GtkTouchDispatcher.cpp +671 -0
  40. package/native/gtk/GtkTouchDispatcher.h +139 -0
  41. package/native/gtk/GtkWindowControl.cpp +223 -0
  42. package/native/gtk/GtkWindowModule.cpp +194 -0
  43. package/native/gtk/GtkWindowModule.h +62 -0
  44. package/native/gtk/PangoTextLayout.cpp +295 -0
  45. package/native/gtk/PangoTextLayout.h +54 -0
  46. package/native/gtk/PangoTextLayoutManager.cpp +140 -0
  47. package/native/gtk/PlatformServicesGtk.cpp +583 -0
  48. package/native/gtk/RnView.cpp +1407 -0
  49. package/native/gtk/RnView.h +339 -0
  50. package/native/gtk/demo_layout_gtk.cpp +57 -0
  51. package/native/gtk/main_gtk.cpp +1981 -0
  52. package/native/gtk/mount_harness_gtk.cpp +143 -0
  53. package/native/tests/TestMainGtk.cpp +25 -0
  54. package/native/tests/notification_stub.cpp +103 -0
  55. package/native/tests/test_accessibility.cpp +251 -0
  56. package/native/tests/test_focus.cpp +194 -0
  57. package/native/tests/test_hittest.cpp +242 -0
  58. package/native/tests/test_image.cpp +135 -0
  59. package/native/tests/test_key_events_gtk.cpp +112 -0
  60. package/native/tests/test_mounting.cpp +415 -0
  61. package/native/tests/test_scrollview.cpp +258 -0
  62. package/native/tests/test_skia_texture.cpp +120 -0
  63. package/native/tests/test_text.cpp +147 -0
  64. package/native/tests/test_textinput.cpp +598 -0
  65. package/native/tests/test_titlebar.cpp +137 -0
  66. package/native/tests/test_titlebar_drag.cpp +138 -0
  67. package/native/tests/test_version.cpp +49 -0
  68. package/native/tests/test_view.cpp +162 -0
  69. package/native/tests/test_viewprops.cpp +254 -0
  70. package/package.json +26 -0
  71. package/react-native.config.js +21 -0
@@ -0,0 +1,415 @@
1
+ // Tests for GtkMountingManager: does a stream of ShadowViewMutations produce
2
+ // the widget tree it describes?
3
+ //
4
+ // This is the layer worth testing hardest. Everything above it is React
5
+ // Native's own code, everything below is GTK's, and the mutation walk is where
6
+ // this project can be wrong on its own.
7
+
8
+ #include "TestHarness.h"
9
+ #include "TreeDump.h"
10
+
11
+ #include "GtkMountingManager.h"
12
+
13
+ #include <string>
14
+ #include <react/renderer/components/view/ViewProps.h>
15
+ #include <react/renderer/graphics/Color.h>
16
+
17
+ #include <cstdint>
18
+
19
+ using facebook::react::ColorComponents;
20
+ using facebook::react::LayoutMetrics;
21
+ using facebook::react::MountingTransaction;
22
+ using facebook::react::ShadowView;
23
+ using facebook::react::ShadowViewMutation;
24
+ using facebook::react::ShadowViewMutationList;
25
+ using facebook::react::SurfaceId;
26
+ using facebook::react::Tag;
27
+ using facebook::react::TransactionTelemetry;
28
+ using facebook::react::ViewProps;
29
+
30
+ namespace {
31
+
32
+ constexpr SurfaceId kSurfaceId = 1;
33
+
34
+ ShadowView makeView(Tag tag,
35
+ float x,
36
+ float y,
37
+ float width,
38
+ float height,
39
+ float red = 0.0F,
40
+ float green = 0.0F,
41
+ float blue = 0.0F) {
42
+ auto props = std::make_shared<ViewProps>();
43
+ props->backgroundColor = facebook::react::colorFromComponents(
44
+ ColorComponents{.red = red, .green = green, .blue = blue, .alpha = 1.0F});
45
+
46
+ LayoutMetrics metrics;
47
+ metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
48
+
49
+ ShadowView view;
50
+ view.componentName = "View";
51
+ view.surfaceId = kSurfaceId;
52
+ view.tag = tag;
53
+ view.props = props;
54
+ view.layoutMetrics = metrics;
55
+ return view;
56
+ }
57
+
58
+ // Applies mutations synchronously. executeMount would queue onto the main loop,
59
+ // which a test has no reason to spin.
60
+ void apply(basalt::GtkMountingManager &manager, ShadowViewMutationList &&mutations) {
61
+ manager.applyTransaction(
62
+ kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
63
+ }
64
+
65
+ int childCount(RnView *view) {
66
+ int count = 0;
67
+ for (GtkWidget *child = gtk_widget_get_first_child(GTK_WIDGET(view)); child != nullptr;
68
+ child = gtk_widget_get_next_sibling(child)) {
69
+ ++count;
70
+ }
71
+ return count;
72
+ }
73
+
74
+ // The tag of the nth child, or -1.
75
+ int childTagAt(RnView *view, int index) {
76
+ int i = 0;
77
+ for (GtkWidget *child = gtk_widget_get_first_child(GTK_WIDGET(view)); child != nullptr;
78
+ child = gtk_widget_get_next_sibling(child)) {
79
+ if (i == index) {
80
+ return rn_view_get_tag(RN_VIEW(child));
81
+ }
82
+ ++i;
83
+ }
84
+ return -1;
85
+ }
86
+
87
+ } // namespace
88
+
89
+ TEST(create_and_insert_builds_the_tree) {
90
+ basalt::GtkMountingManager manager;
91
+ RnView *root = manager.createSurfaceRoot(kSurfaceId);
92
+
93
+ ShadowViewMutationList mutations;
94
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(10, 0, 0, 100, 50)));
95
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(11, 0, 60, 100, 50)));
96
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(10, 0, 0, 100, 50), 0));
97
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(11, 0, 60, 100, 50), 1));
98
+ apply(manager, std::move(mutations));
99
+
100
+ EXPECT_EQ(childCount(root), 2);
101
+ EXPECT_EQ(childTagAt(root, 0), 10);
102
+ EXPECT_EQ(childTagAt(root, 1), 11);
103
+
104
+ manager.destroySurfaceRoot(kSurfaceId);
105
+ }
106
+
107
+ TEST(insert_honours_the_index) {
108
+ basalt::GtkMountingManager manager;
109
+ RnView *root = manager.createSurfaceRoot(kSurfaceId);
110
+
111
+ ShadowViewMutationList first;
112
+ first.push_back(ShadowViewMutation::CreateMutation(makeView(20, 0, 0, 10, 10)));
113
+ first.push_back(ShadowViewMutation::CreateMutation(makeView(21, 0, 0, 10, 10)));
114
+ first.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(20, 0, 0, 10, 10), 0));
115
+ first.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(21, 0, 0, 10, 10), 1));
116
+ apply(manager, std::move(first));
117
+
118
+ // Fabric numbers inserts by their position in the parent's final child list,
119
+ // so a later mutation can land between two existing children.
120
+ ShadowViewMutationList second;
121
+ second.push_back(ShadowViewMutation::CreateMutation(makeView(22, 0, 0, 10, 10)));
122
+ second.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(22, 0, 0, 10, 10), 1));
123
+ apply(manager, std::move(second));
124
+
125
+ EXPECT_EQ(childCount(root), 3);
126
+ EXPECT_EQ(childTagAt(root, 0), 20);
127
+ EXPECT_EQ(childTagAt(root, 1), 22);
128
+ EXPECT_EQ(childTagAt(root, 2), 21);
129
+
130
+ manager.destroySurfaceRoot(kSurfaceId);
131
+ }
132
+
133
+ TEST(remove_detaches_but_does_not_destroy) {
134
+ basalt::GtkMountingManager manager;
135
+ RnView *root = manager.createSurfaceRoot(kSurfaceId);
136
+
137
+ ShadowViewMutationList mutations;
138
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(30, 0, 0, 10, 10)));
139
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(30, 0, 0, 10, 10), 0));
140
+ apply(manager, std::move(mutations));
141
+
142
+ ShadowViewMutationList removal;
143
+ removal.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, makeView(30, 0, 0, 10, 10), 0));
144
+ apply(manager, std::move(removal));
145
+
146
+ // Detached from the parent...
147
+ EXPECT_EQ(childCount(root), 0);
148
+
149
+ // ...but still alive, because a re-Insert may follow before the Delete. This
150
+ // is the invariant the registry's strong reference exists for.
151
+ ShadowViewMutationList reinsert;
152
+ reinsert.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(30, 0, 0, 10, 10), 0));
153
+ apply(manager, std::move(reinsert));
154
+
155
+ EXPECT_EQ(childCount(root), 1);
156
+ EXPECT_EQ(childTagAt(root, 0), 30);
157
+
158
+ manager.destroySurfaceRoot(kSurfaceId);
159
+ }
160
+
161
+ TEST(update_changes_frame_without_reparenting) {
162
+ basalt::GtkMountingManager manager;
163
+ RnView *root = manager.createSurfaceRoot(kSurfaceId);
164
+
165
+ ShadowViewMutationList mutations;
166
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(40, 5, 6, 100, 50)));
167
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(40, 5, 6, 100, 50), 0));
168
+ apply(manager, std::move(mutations));
169
+
170
+ ShadowViewMutationList update;
171
+ update.push_back(ShadowViewMutation::UpdateMutation(
172
+ makeView(40, 5, 6, 100, 50), makeView(40, 7, 8, 200, 75), kSurfaceId));
173
+ apply(manager, std::move(update));
174
+
175
+ EXPECT_EQ(childCount(root), 1);
176
+
177
+ graphene_rect_t frame;
178
+ rn_view_get_frame(RN_VIEW(gtk_widget_get_first_child(GTK_WIDGET(root))), &frame);
179
+ EXPECT_NEAR(frame.origin.x, 7.0, 0.001);
180
+ EXPECT_NEAR(frame.origin.y, 8.0, 0.001);
181
+ EXPECT_NEAR(frame.size.width, 200.0, 0.001);
182
+ EXPECT_NEAR(frame.size.height, 75.0, 0.001);
183
+
184
+ manager.destroySurfaceRoot(kSurfaceId);
185
+ }
186
+
187
+ TEST(delete_removes_the_view_from_the_registry) {
188
+ basalt::GtkMountingManager manager;
189
+ RnView *root = manager.createSurfaceRoot(kSurfaceId);
190
+
191
+ ShadowViewMutationList mutations;
192
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(50, 0, 0, 10, 10)));
193
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(50, 0, 0, 10, 10), 0));
194
+ apply(manager, std::move(mutations));
195
+
196
+ ShadowViewMutationList teardown;
197
+ teardown.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, makeView(50, 0, 0, 10, 10), 0));
198
+ teardown.push_back(ShadowViewMutation::DeleteMutation(makeView(50, 0, 0, 10, 10)));
199
+ apply(manager, std::move(teardown));
200
+
201
+ EXPECT_EQ(childCount(root), 0);
202
+
203
+ // A stray Insert for a deleted tag must be ignored rather than crash, since
204
+ // that is what a bug upstream would look like from here.
205
+ ShadowViewMutationList stray;
206
+ stray.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(50, 0, 0, 10, 10), 0));
207
+ apply(manager, std::move(stray));
208
+ EXPECT_EQ(childCount(root), 0);
209
+
210
+ manager.destroySurfaceRoot(kSurfaceId);
211
+ }
212
+
213
+ TEST(nested_children_are_parented_to_their_own_parent) {
214
+ basalt::GtkMountingManager manager;
215
+ RnView *root = manager.createSurfaceRoot(kSurfaceId);
216
+
217
+ ShadowViewMutationList mutations;
218
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(60, 0, 0, 200, 200)));
219
+ mutations.push_back(ShadowViewMutation::CreateMutation(makeView(61, 10, 10, 50, 50)));
220
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, makeView(60, 0, 0, 200, 200), 0));
221
+ // parentTag 60, not the surface root.
222
+ mutations.push_back(ShadowViewMutation::InsertMutation(60, makeView(61, 10, 10, 50, 50), 0));
223
+ apply(manager, std::move(mutations));
224
+
225
+ EXPECT_EQ(childCount(root), 1);
226
+ RnView *outer = RN_VIEW(gtk_widget_get_first_child(GTK_WIDGET(root)));
227
+ EXPECT_EQ(rn_view_get_tag(outer), 60);
228
+ EXPECT_EQ(childCount(outer), 1);
229
+ EXPECT_EQ(childTagAt(outer, 0), 61);
230
+
231
+ manager.destroySurfaceRoot(kSurfaceId);
232
+ }
233
+
234
+ TEST(surface_root_is_reused_not_recreated) {
235
+ basalt::GtkMountingManager manager;
236
+ RnView *first = manager.createSurfaceRoot(kSurfaceId);
237
+ RnView *again = manager.createSurfaceRoot(kSurfaceId);
238
+
239
+ // Fabric emits no Create for a root, so asking twice must not produce two.
240
+ EXPECT(first == again);
241
+ EXPECT(manager.getSurfaceRoot(kSurfaceId) == first);
242
+
243
+ // In Fabric a SurfaceId *is* the root's tag.
244
+ EXPECT_EQ(rn_view_get_tag(first), static_cast<int>(kSurfaceId));
245
+
246
+ manager.destroySurfaceRoot(kSurfaceId);
247
+ EXPECT(manager.getSurfaceRoot(kSurfaceId) == nullptr);
248
+ }
249
+
250
+ TEST(has_component_matches_the_registered_descriptors) {
251
+ basalt::GtkMountingManager manager;
252
+
253
+ EXPECT(manager.hasComponent("View"));
254
+ EXPECT(manager.hasComponent("RootView"));
255
+ EXPECT(manager.hasComponent("Paragraph"));
256
+ EXPECT(manager.hasComponent("Image"));
257
+ EXPECT(manager.hasComponent("ScrollView"));
258
+ EXPECT(manager.hasComponent("TextInput"));
259
+ EXPECT(manager.hasComponent("ActivityIndicatorView"));
260
+ EXPECT(manager.hasComponent("Switch"));
261
+ EXPECT(manager.hasComponent("ModalHostView"));
262
+ EXPECT(manager.hasComponent("PullToRefreshView"));
263
+ // What React Native substitutes for a component nobody registered. Mounting
264
+ // it is what turns "nothing happens" into a view the tree dump can show.
265
+ EXPECT(manager.hasComponent("UnimplementedNativeView"));
266
+
267
+ // Claiming a component without a GTK peer is worse than admitting the gap:
268
+ // the registry would build shadow nodes nothing can mount.
269
+ //
270
+ // `ActivityIndicator` is here rather than above on purpose: the component
271
+ // React Native actually mounts is `ActivityIndicatorView`, and answering to
272
+ // the name a reader expects would be answering to a name nothing sends.
273
+ EXPECT(!manager.hasComponent("Slider"));
274
+ EXPECT(!manager.hasComponent("ActivityIndicator"));
275
+ EXPECT(!manager.hasComponent("SomethingNobodyHasHeardOf"));
276
+ }
277
+
278
+ // Hover runs only for views that asked for it, and the ask arrives as props on
279
+ // every Create and Update. Recorded by the mutation walk, so a cursor crossing
280
+ // an app that uses no hover costs a hit test and nothing else.
281
+ TEST(a_hover_listener_is_remembered_from_the_props) {
282
+ basalt::GtkMountingManager manager;
283
+ manager.createSurfaceRoot(kSurfaceId);
284
+
285
+ ShadowView listening = makeView(10, 0, 0, 100, 50);
286
+ auto props = std::make_shared<ViewProps>();
287
+ props->events[facebook::react::ViewEvents::Offset::PointerEnter] = true;
288
+ listening.props = props;
289
+
290
+ ShadowViewMutationList mutations;
291
+ mutations.push_back(ShadowViewMutation::CreateMutation(listening));
292
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, listening, 0));
293
+ apply(manager, std::move(mutations));
294
+
295
+ EXPECT_EQ(manager.hoverListenersForTag(10),
296
+ static_cast<std::uint16_t>(basalt::HoverListenerEnter));
297
+ // A view that never asked is not stored at all.
298
+ EXPECT_EQ(manager.hoverListenersForTag(kSurfaceId),
299
+ static_cast<std::uint16_t>(basalt::HoverListenerNone));
300
+
301
+ // An update can take the listener away again, and leaving the old mask behind
302
+ // would mean dispatching to a view that stopped listening.
303
+ ShadowViewMutationList updates;
304
+ updates.push_back(
305
+ ShadowViewMutation::UpdateMutation(listening, makeView(10, 0, 0, 100, 50), kSurfaceId));
306
+ apply(manager, std::move(updates));
307
+ EXPECT_EQ(manager.hoverListenersForTag(10),
308
+ static_cast<std::uint16_t>(basalt::HoverListenerNone));
309
+
310
+ manager.destroySurfaceRoot(kSurfaceId);
311
+ }
312
+
313
+ // --- transformOrigin --------------------------------------------------------
314
+ //
315
+ // `resolveTransform` folds the origin into the matrix, and all three hosts
316
+ // call it -- so this has been "passed through but never exercised" since it
317
+ // was written. What makes it checkable without a display is that the anchor
318
+ // is a claim about a point: scaling about the top-left must leave the
319
+ // top-left corner exactly where it was.
320
+ //
321
+ // The hosts anchor a transform at the view's centre, which is what an
322
+ // untouched `transformOrigin` means, so the matrix is in centre-relative
323
+ // coordinates and the top-left corner is at (-w/2, -h/2).
324
+
325
+ namespace {
326
+
327
+ ShadowView makeTransformed(Tag tag,
328
+ float width,
329
+ float height,
330
+ facebook::react::Transform transform,
331
+ facebook::react::TransformOrigin origin = {}) {
332
+ // Fresh props rather than a copy of makeView's: BaseViewProps has a deleted
333
+ // copy constructor at React Native 0.86, which is what a current Expo app
334
+ // installs. Copying compiled against main and broke the version an app
335
+ // actually has -- found by building a real create-expo-app, not here.
336
+ ShadowView view = makeView(tag, 0.0F, 0.0F, width, height);
337
+ auto props = std::make_shared<ViewProps>();
338
+ props->transform = transform;
339
+ props->transformOrigin = origin;
340
+ view.props = props;
341
+ return view;
342
+ }
343
+
344
+ // The dump this host wrote, as a string.
345
+ std::string treeOf(RnView *root) {
346
+ char *described = rn_view_describe_tree(root);
347
+ std::string tree = described != nullptr ? described : "";
348
+ g_free(described);
349
+ return tree;
350
+ }
351
+
352
+ } // namespace
353
+
354
+ TEST(transform_origin_anchors_the_corner_it_names) {
355
+ basalt::GtkMountingManager manager;
356
+ RnView *root = manager.createSurfaceRoot(kSurfaceId);
357
+
358
+ using facebook::react::Transform;
359
+ using facebook::react::TransformOrigin;
360
+ using facebook::react::UnitType;
361
+ using facebook::react::ValueUnit;
362
+
363
+ // Doubled about the top-left of a 100x50 view.
364
+ TransformOrigin topLeft;
365
+ topLeft.xy = {ValueUnit(0.0F, UnitType::Point), ValueUnit(0.0F, UnitType::Point)};
366
+
367
+ ShadowViewMutationList mutations;
368
+ mutations.push_back(ShadowViewMutation::CreateMutation(
369
+ makeTransformed(30, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F), topLeft)));
370
+ mutations.push_back(ShadowViewMutation::InsertMutation(
371
+ kSurfaceId,
372
+ makeTransformed(30, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F), topLeft),
373
+ 0));
374
+ apply(manager, std::move(mutations));
375
+
376
+ const auto matrix = basalt::testing::transformIn(treeOf(root), 30);
377
+ EXPECT_EQ(matrix[0], 2.0);
378
+ EXPECT_EQ(matrix[3], 2.0);
379
+
380
+ // The corner the origin names, in the centre-relative coordinates the
381
+ // matrix is written in, has to come back to itself: (-50,-25) scaled by two
382
+ // is (-100,-50), so the translation has to put 50 and 25 back.
383
+ const double cornerX = matrix[0] * -50.0 + matrix[2] * -25.0 + matrix[4];
384
+ const double cornerY = matrix[1] * -50.0 + matrix[3] * -25.0 + matrix[5];
385
+ EXPECT_EQ(cornerX, -50.0);
386
+ EXPECT_EQ(cornerY, -25.0);
387
+
388
+ manager.destroySurfaceRoot(kSurfaceId);
389
+ }
390
+
391
+ // And with no origin set, the centre is the anchor -- which is the default
392
+ // every host relies on, and the reason an unset origin must not be folded in
393
+ // as (0,0).
394
+ TEST(no_transform_origin_anchors_the_centre) {
395
+ basalt::GtkMountingManager manager;
396
+ RnView *root = manager.createSurfaceRoot(kSurfaceId);
397
+
398
+ using facebook::react::Transform;
399
+
400
+ ShadowViewMutationList mutations;
401
+ mutations.push_back(ShadowViewMutation::CreateMutation(
402
+ makeTransformed(31, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F))));
403
+ mutations.push_back(ShadowViewMutation::InsertMutation(
404
+ kSurfaceId, makeTransformed(31, 100.0F, 50.0F, Transform::Scale(2.0F, 2.0F, 1.0F)), 0));
405
+ apply(manager, std::move(mutations));
406
+
407
+ const auto matrix = basalt::testing::transformIn(treeOf(root), 31);
408
+ EXPECT_EQ(matrix[0], 2.0);
409
+ EXPECT_EQ(matrix[3], 2.0);
410
+ // No translation at all: the centre is already the anchor.
411
+ EXPECT_EQ(matrix[4], 0.0);
412
+ EXPECT_EQ(matrix[5], 0.0);
413
+
414
+ manager.destroySurfaceRoot(kSurfaceId);
415
+ }
@@ -0,0 +1,258 @@
1
+ // Tests for the fling GTK hands over half-finished.
2
+ //
3
+ // `<ScrollView>` itself is exercised end to end by `scripts/compare_hosts.sh`
4
+ // running `js/scroll.js` against both hosts, which is a better test than
5
+ // anything here -- it compares one desktop's answer to another's. Momentum is
6
+ // the part that cannot reach: a real fling needs a touchscreen, and the frame
7
+ // clock that drives it needs a mapped window and two seconds of main loop.
8
+ //
9
+ // So the tick is driven directly instead. `fling` and `advanceFling` enter
10
+ // exactly where GDK's `decelerate` signal and the frame clock do and skip
11
+ // nothing above them; what goes untested is that GTK emits `decelerate` at all,
12
+ // which is the same gap every synthesised input in this project leaves.
13
+ //
14
+ // The manager is built standalone rather than through GtkMountingManager: it
15
+ // takes an emitter lookup and nothing else, and a lookup that answers nothing
16
+ // is honest here -- an EventEmitter built by hand has no EventDispatcher, so
17
+ // `onMomentumScrollBegin` would go nowhere anyway. What is observable is where
18
+ // the content actually sits, which is what a fling is for.
19
+
20
+ #include "TestHarness.h"
21
+
22
+ #include "GtkScrollView.h"
23
+ #include "RnView.h"
24
+
25
+ #include <react/renderer/components/scrollview/ScrollViewProps.h>
26
+ #include <react/renderer/components/scrollview/ScrollViewShadowNode.h>
27
+ #include <react/renderer/components/scrollview/ScrollViewState.h>
28
+
29
+ #include <memory>
30
+ #include <sstream>
31
+
32
+ using facebook::react::LayoutMetrics;
33
+ using facebook::react::Point;
34
+ using facebook::react::Rect;
35
+ using facebook::react::ScrollViewProps;
36
+ using facebook::react::ScrollViewShadowNode;
37
+ using facebook::react::ScrollViewState;
38
+ using facebook::react::ShadowNodeFamily;
39
+ using facebook::react::ShadowView;
40
+ using facebook::react::Size;
41
+ using facebook::react::SurfaceId;
42
+ using facebook::react::Tag;
43
+
44
+ namespace {
45
+
46
+ constexpr SurfaceId kSurfaceId = 1;
47
+ constexpr double kFrame = 1.0 / 60.0;
48
+
49
+ ShadowView makeScrollView(Tag tag,
50
+ float width,
51
+ float height,
52
+ float contentWidth,
53
+ float contentHeight,
54
+ float decelerationRate = 0,
55
+ bool pagingEnabled = false) {
56
+ LayoutMetrics metrics;
57
+ metrics.frame = {.origin = {.x = 0, .y = 0}, .size = {.width = width, .height = height}};
58
+
59
+ auto props = std::make_shared<ScrollViewProps>();
60
+ props->decelerationRate = decelerationRate;
61
+ props->pagingEnabled = pagingEnabled;
62
+
63
+ ScrollViewState data;
64
+ data.contentOffset = Point{0, 0};
65
+ data.contentBoundingRect = Rect{.origin = {.x = 0, .y = 0},
66
+ .size = Size{.width = contentWidth, .height = contentHeight}};
67
+
68
+ ShadowView view;
69
+ view.componentName = "ScrollView";
70
+ view.surfaceId = kSurfaceId;
71
+ view.tag = tag;
72
+ view.props = props;
73
+ view.layoutMetrics = metrics;
74
+ // An empty family: nothing can be committed back through it, so the
75
+ // unthrottled state write-back is not observable from here.
76
+ view.state = std::make_shared<const ScrollViewShadowNode::ConcreteState>(
77
+ std::make_shared<const ScrollViewState>(data), ShadowNodeFamily::Weak{});
78
+ return view;
79
+ }
80
+
81
+ // A manager with one ScrollView registered, and the widget it is registered
82
+ // against. The widget is owned by the caller's scope, which is why the tests
83
+ // sink and unref it rather than parenting it into anything.
84
+ struct Scroller {
85
+ basalt::GtkScrollViewManager manager;
86
+ RnView *view;
87
+ Tag tag;
88
+
89
+ Scroller(Tag tag, float contentHeight, float decelerationRate = 0, bool pagingEnabled = false)
90
+ : manager([](Tag) { return facebook::react::EventEmitter::Shared{}; }),
91
+ view(rn_view_new(static_cast<int>(tag))),
92
+ tag(tag) {
93
+ g_object_ref_sink(view);
94
+ rn_view_set_frame(view, 0, 0, 400, 300);
95
+ manager.update(
96
+ view,
97
+ makeScrollView(tag, 400, 300, 400, contentHeight, decelerationRate, pagingEnabled));
98
+ }
99
+
100
+ ~Scroller() {
101
+ manager.remove(tag);
102
+ g_object_unref(view);
103
+ }
104
+
105
+ Scroller(const Scroller &) = delete;
106
+ Scroller &operator=(const Scroller &) = delete;
107
+
108
+ double offsetY() const {
109
+ double x = 0;
110
+ double y = 0;
111
+ rn_view_get_scroll_offset(view, &x, &y);
112
+ return y;
113
+ }
114
+
115
+ // Runs the fling to a standstill and returns how many frames it took.
116
+ // Bounded, so a fling that never stops fails the assertion rather than
117
+ // hanging the suite.
118
+ int settle() {
119
+ int frames = 0;
120
+ while (manager.advanceFling(tag, kFrame) && frames < 6000) {
121
+ frames++;
122
+ }
123
+ return frames + 1;
124
+ }
125
+ };
126
+
127
+ } // namespace
128
+
129
+ TEST(scrollview_a_fling_coasts_and_stops) {
130
+ Scroller scroller(10, 4000);
131
+ EXPECT(scroller.manager.fling(scroller.tag, 0, 1200));
132
+ const int frames = scroller.settle();
133
+
134
+ // The same flight core/ScrollMomentum.h is tested for, arriving as an offset.
135
+ EXPECT(scroller.offsetY() > 450);
136
+ EXPECT(scroller.offsetY() < 750);
137
+ EXPECT(frames > 60);
138
+ EXPECT(frames < 200);
139
+ // And it is over: another tick moves nothing.
140
+ const double settled = scroller.offsetY();
141
+ EXPECT(!scroller.manager.advanceFling(scroller.tag, kFrame));
142
+ EXPECT_NEAR(scroller.offsetY(), settled, 0.001);
143
+ }
144
+
145
+ TEST(scrollview_a_fling_stops_at_the_end_of_the_content) {
146
+ // 300pt of viewport over 500pt of content: 200pt to travel, and a fling with
147
+ // enough velocity to want far more.
148
+ Scroller scroller(11, 500);
149
+ EXPECT(scroller.manager.fling(scroller.tag, 0, 4000));
150
+ const int frames = scroller.settle();
151
+
152
+ EXPECT_NEAR(scroller.offsetY(), 200.0, 0.001);
153
+ // The assertion that matters: it noticed. A fling that hits an edge still has
154
+ // velocity left, and without the check it would coast against the stop for a
155
+ // second before reporting that it had finished -- during which a list would
156
+ // believe it was still scrolling.
157
+ EXPECT(frames < 30);
158
+ }
159
+
160
+ TEST(scrollview_a_fling_is_clamped_at_the_top) {
161
+ Scroller scroller(12, 4000);
162
+ // Already at the top, flung further up: there is nowhere to go.
163
+ EXPECT(scroller.manager.fling(scroller.tag, 0, -1500));
164
+ scroller.settle();
165
+ EXPECT_NEAR(scroller.offsetY(), 0.0, 0.001);
166
+ }
167
+
168
+ TEST(scrollview_a_slow_flick_starts_no_fling) {
169
+ Scroller scroller(13, 4000);
170
+ // Below the model's stopping speed. Reporting a momentum scroll that began
171
+ // and ended in the same frame is worse than reporting none.
172
+ EXPECT(!scroller.manager.fling(scroller.tag, 0, 5));
173
+ EXPECT(!scroller.manager.advanceFling(scroller.tag, kFrame));
174
+ EXPECT_NEAR(scroller.offsetY(), 0.0, 0.001);
175
+ }
176
+
177
+ TEST(scrollview_a_scrollTo_command_takes_the_list_off_a_fling) {
178
+ Scroller scroller(14, 4000);
179
+ EXPECT(scroller.manager.fling(scroller.tag, 0, 1200));
180
+ scroller.manager.advanceFling(scroller.tag, kFrame);
181
+
182
+ scroller.manager.dispatchCommand(scroller.tag, "scrollTo", folly::dynamic::array(0, 900, false));
183
+ EXPECT_NEAR(scroller.offsetY(), 900.0, 0.001);
184
+ // The fling is gone rather than paused: an app that asked for an offset means
185
+ // that offset, not that offset plus wherever the coast was heading.
186
+ EXPECT(!scroller.manager.advanceFling(scroller.tag, kFrame));
187
+ EXPECT_NEAR(scroller.offsetY(), 900.0, 0.001);
188
+ }
189
+
190
+ TEST(scrollview_decelerationRate_shortens_a_fling) {
191
+ Scroller normal(15, 4000);
192
+ normal.manager.fling(normal.tag, 0, 1200);
193
+ normal.settle();
194
+
195
+ Scroller fast(16, 4000, basalt::ScrollMomentum::kFastDeceleration);
196
+ fast.manager.fling(fast.tag, 0, 1200);
197
+ fast.settle();
198
+
199
+ // React Native's prop, reaching the model. 'fast' is the smaller number and
200
+ // the shorter fling.
201
+ EXPECT(fast.offsetY() < normal.offsetY());
202
+ }
203
+
204
+ // --- Paging ------------------------------------------------------------------
205
+ //
206
+ // The arithmetic is tested in test_scroll_snap.cpp; what these assert is the
207
+ // wiring -- that the props reach the manager, and that a fling on a paging list
208
+ // settles on a boundary instead of coasting.
209
+ //
210
+ // The container is 300 tall, so a page is 300.
211
+
212
+ TEST(scrollview_a_fling_on_a_paging_list_settles_on_a_page) {
213
+ Scroller scroller(11, 4000, 0, /*pagingEnabled=*/true);
214
+
215
+ // Returns false: a snapping list does not coast, so there is no fling to
216
+ // report having started.
217
+ EXPECT(!scroller.manager.fling(scroller.tag, 0, 1200));
218
+ // The settle is an animation rather than a fling, so it is advanced rather
219
+ // than coasted. Sixty frames is twice the curve's length.
220
+ for (int i = 0; i < 60; i++) {
221
+ scroller.manager.advanceAnimation(scroller.tag, 1.0 / 60.0);
222
+ }
223
+ EXPECT_EQ(scroller.offsetY(), 300.0);
224
+ }
225
+
226
+ TEST(scrollview_a_fling_backwards_settles_on_the_boundary_behind) {
227
+ // Twenty pixels into the second page, flicked back: the answer is that page's
228
+ // start, not the page before it. "The next point in the direction flicked" is
229
+ // symmetric -- forwards from here would be 600 -- and it is what CSS
230
+ // scroll-snap does. Going back two boundaries would mean a small flick could
231
+ // travel further than a large one.
232
+ Scroller scroller(12, 4000, 0, /*pagingEnabled=*/true);
233
+ scroller.manager.dispatchCommand(scroller.tag, "scrollTo", folly::dynamic::array(0, 320, false));
234
+ EXPECT(!scroller.manager.fling(scroller.tag, 0, -1200));
235
+ for (int i = 0; i < 60; i++) {
236
+ scroller.manager.advanceAnimation(scroller.tag, 1.0 / 60.0);
237
+ }
238
+ EXPECT_EQ(scroller.offsetY(), 300.0);
239
+ }
240
+
241
+ TEST(scrollview_a_fling_back_from_a_boundary_reaches_the_previous_page) {
242
+ // Exactly on a boundary there is nothing behind to settle on, so the flick
243
+ // takes the page before it -- which is what stops a list getting stuck.
244
+ Scroller scroller(14, 4000, 0, /*pagingEnabled=*/true);
245
+ scroller.manager.dispatchCommand(scroller.tag, "scrollTo", folly::dynamic::array(0, 300, false));
246
+ EXPECT(!scroller.manager.fling(scroller.tag, 0, -1200));
247
+ for (int i = 0; i < 60; i++) {
248
+ scroller.manager.advanceAnimation(scroller.tag, 1.0 / 60.0);
249
+ }
250
+ EXPECT_EQ(scroller.offsetY(), 0.0);
251
+ }
252
+
253
+ TEST(scrollview_a_fling_on_a_plain_list_still_coasts) {
254
+ // The guard that matters: paging is off for almost every list, and this path
255
+ // runs at the end of every fling.
256
+ Scroller scroller(13, 4000);
257
+ EXPECT(scroller.manager.fling(scroller.tag, 0, 1200));
258
+ }