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.
- package/LICENSE +21 -0
- package/cli/runLinux.js +29 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +362 -0
- package/native/gtk/ColorSchemeGtk.cpp +58 -0
- package/native/gtk/ComponentRegistryGtk.cpp +101 -0
- package/native/gtk/FontRegistryFontconfig.cpp +127 -0
- package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
- package/native/gtk/GtkAnimationChoreographer.h +49 -0
- package/native/gtk/GtkDropTarget.cpp +269 -0
- package/native/gtk/GtkDropTarget.h +40 -0
- package/native/gtk/GtkFocus.cpp +296 -0
- package/native/gtk/GtkFocus.h +113 -0
- package/native/gtk/GtkImageLoader.cpp +128 -0
- package/native/gtk/GtkImageLoader.h +87 -0
- package/native/gtk/GtkKeyEvents.cpp +91 -0
- package/native/gtk/GtkKeyEvents.h +21 -0
- package/native/gtk/GtkMenuBar.cpp +45 -0
- package/native/gtk/GtkMountingManager.cpp +1027 -0
- package/native/gtk/GtkMountingManager.h +146 -0
- package/native/gtk/GtkRunLoopObserver.cpp +77 -0
- package/native/gtk/GtkRunLoopObserver.h +41 -0
- package/native/gtk/GtkScrollView.cpp +626 -0
- package/native/gtk/GtkScrollView.h +209 -0
- package/native/gtk/GtkSkiaContext.cpp +248 -0
- package/native/gtk/GtkSkiaContext.h +62 -0
- package/native/gtk/GtkSkiaModule.cpp +80 -0
- package/native/gtk/GtkSkiaModule.h +51 -0
- package/native/gtk/GtkSkiaPeer.cpp +285 -0
- package/native/gtk/GtkSkiaPeer.h +63 -0
- package/native/gtk/GtkTextInput.cpp +428 -0
- package/native/gtk/GtkTextInput.h +123 -0
- package/native/gtk/GtkTextPeer.cpp +425 -0
- package/native/gtk/GtkTextPeer.h +87 -0
- package/native/gtk/GtkTitleBar.cpp +273 -0
- package/native/gtk/GtkTitleBar.h +96 -0
- package/native/gtk/GtkTitleBarLayout.cpp +45 -0
- package/native/gtk/GtkTitleBarLayout.h +36 -0
- package/native/gtk/GtkTouchDispatcher.cpp +671 -0
- package/native/gtk/GtkTouchDispatcher.h +139 -0
- package/native/gtk/GtkWindowControl.cpp +223 -0
- package/native/gtk/GtkWindowModule.cpp +194 -0
- package/native/gtk/GtkWindowModule.h +62 -0
- package/native/gtk/PangoTextLayout.cpp +295 -0
- package/native/gtk/PangoTextLayout.h +54 -0
- package/native/gtk/PangoTextLayoutManager.cpp +140 -0
- package/native/gtk/PlatformServicesGtk.cpp +583 -0
- package/native/gtk/RnView.cpp +1407 -0
- package/native/gtk/RnView.h +339 -0
- package/native/gtk/demo_layout_gtk.cpp +57 -0
- package/native/gtk/main_gtk.cpp +1981 -0
- package/native/gtk/mount_harness_gtk.cpp +143 -0
- package/native/tests/TestMainGtk.cpp +25 -0
- package/native/tests/notification_stub.cpp +103 -0
- package/native/tests/test_accessibility.cpp +251 -0
- package/native/tests/test_focus.cpp +194 -0
- package/native/tests/test_hittest.cpp +242 -0
- package/native/tests/test_image.cpp +135 -0
- package/native/tests/test_key_events_gtk.cpp +112 -0
- package/native/tests/test_mounting.cpp +415 -0
- package/native/tests/test_scrollview.cpp +258 -0
- package/native/tests/test_skia_texture.cpp +120 -0
- package/native/tests/test_text.cpp +147 -0
- package/native/tests/test_textinput.cpp +598 -0
- package/native/tests/test_titlebar.cpp +137 -0
- package/native/tests/test_titlebar_drag.cpp +138 -0
- package/native/tests/test_version.cpp +49 -0
- package/native/tests/test_view.cpp +162 -0
- package/native/tests/test_viewprops.cpp +254 -0
- package/package.json +26 -0
- 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
|
+
}
|