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,671 @@
1
+ #include "GtkTouchDispatcher.h"
2
+
3
+ #include "Gestures.h"
4
+
5
+ #include <react/renderer/components/view/PointerEvent.h>
6
+ #include <react/renderer/components/view/TouchEvent.h>
7
+
8
+ #include <chrono>
9
+ #include <cstdint>
10
+ #include <vector>
11
+
12
+ namespace basalt {
13
+
14
+ using facebook::react::HighResTimeStamp;
15
+ using facebook::react::Point;
16
+ using facebook::react::PointerEvent;
17
+ using facebook::react::Tag;
18
+ using facebook::react::TouchEventEmitter;
19
+ using facebook::react::Touch;
20
+ using facebook::react::TouchEvent;
21
+ using facebook::react::Touches;
22
+
23
+ namespace {
24
+
25
+ // A desktop pointer is one touch point, and React Native identifies touches by
26
+ // number. Zero is the first (and here only) finger. The same number identifies
27
+ // the pointer in pointer events, where React Native keys its hover tracking by
28
+ // it -- and where zero is also what React Native's own macOS host uses for a
29
+ // mouse.
30
+ constexpr int kPointerIdentifier = 0;
31
+
32
+ // The payload for a hovering pointer, at a point in root coordinates with the
33
+ // target's origin in the same space.
34
+ PointerEvent hoverEvent(double x, double y, double originX, double originY) {
35
+ PointerEvent event{};
36
+ event.pointerId = kPointerIdentifier;
37
+ event.pointerType = "mouse";
38
+ event.clientPoint = Point{.x = static_cast<facebook::react::Float>(x),
39
+ .y = static_cast<facebook::react::Float>(y)};
40
+ event.screenPoint = event.clientPoint;
41
+ event.offsetPoint = Point{.x = static_cast<facebook::react::Float>(x - originX),
42
+ .y = static_cast<facebook::react::Float>(y - originY)};
43
+ event.width = 1;
44
+ event.height = 1;
45
+ // A hovering mouse presses nothing. `button` is -1 rather than 0 because 0 is
46
+ // the left button; -1 is W3C's "no button changed state".
47
+ event.button = -1;
48
+ event.buttons = 0;
49
+ event.isPrimary = true;
50
+ event.timeStamp = HighResTimeStamp::now();
51
+ return event;
52
+ }
53
+
54
+ } // namespace
55
+
56
+ GtkTouchDispatcher::GtkTouchDispatcher(GtkMountingManager *mountingManager, RnView *surfaceRoot)
57
+ : mountingManager_(mountingManager), surfaceRoot_(surfaceRoot) {
58
+ clickGesture_ = gtk_gesture_click_new();
59
+ // Button 0 means every button, which is still what this wants -- but they no
60
+ // longer all arrive as touches. The gesture is asked which one it was and
61
+ // only the primary presses anything; see core/PointerButtons.h.
62
+ gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(clickGesture_), 0);
63
+ g_signal_connect(clickGesture_, "pressed", G_CALLBACK(onPressed), this);
64
+ g_signal_connect(clickGesture_, "released", G_CALLBACK(onReleased), this);
65
+ g_signal_connect(clickGesture_, "cancel", G_CALLBACK(onCancelled), this);
66
+ gtk_widget_add_controller(GTK_WIDGET(surfaceRoot_), GTK_EVENT_CONTROLLER(clickGesture_));
67
+ // Weak, so it becomes NULL when the widget takes it down rather than
68
+ // dangling. The destructor has to reach these and cannot know which went
69
+ // first; see there.
70
+ g_object_add_weak_pointer(G_OBJECT(clickGesture_), reinterpret_cast<gpointer *>(&clickGesture_));
71
+
72
+ motionController_ = gtk_event_controller_motion_new();
73
+ g_signal_connect(motionController_, "motion", G_CALLBACK(onMotion), this);
74
+ // The cursor leaving the window is not a motion event, and without it a view
75
+ // that was hovered when the pointer left the window stays hovered forever.
76
+ g_signal_connect(motionController_, "leave", G_CALLBACK(onPointerLeft), this);
77
+ gtk_widget_add_controller(GTK_WIDGET(surfaceRoot_), motionController_);
78
+ g_object_add_weak_pointer(G_OBJECT(motionController_),
79
+ reinterpret_cast<gpointer *>(&motionController_));
80
+ }
81
+
82
+ GtkTouchDispatcher::~GtkTouchDispatcher() {
83
+ // Disconnect, which this used to think was unnecessary.
84
+ //
85
+ // `gtk_widget_add_controller` takes ownership, so the controllers do die with
86
+ // the widget -- but they outlive *this*, because the dispatcher is reset
87
+ // before the window is destroyed. Both teardown paths do it in that order:
88
+ // `closeHostWindow` resets the dispatcher and then calls
89
+ // `gtk_window_destroy`, and `shutdown` does the same. In between, destroying
90
+ // the window unmaps it, which makes GTK deliver a leave crossing to a
91
+ // controller whose callback still holds this pointer.
92
+ //
93
+ // That is a segfault in `dispatchHoverLeave` reading a mounting manager
94
+ // through freed memory, and it is what the crash report said.
95
+ //
96
+ // Weak pointers rather than a flag, because either object may already be
97
+ // gone: a widget destroyed first takes its controllers with it and NULLs
98
+ // these, and there is then nothing to disconnect.
99
+ if (clickGesture_ != nullptr) {
100
+ g_signal_handlers_disconnect_by_data(clickGesture_, this);
101
+ g_object_remove_weak_pointer(G_OBJECT(clickGesture_),
102
+ reinterpret_cast<gpointer *>(&clickGesture_));
103
+ }
104
+ if (motionController_ != nullptr) {
105
+ g_signal_handlers_disconnect_by_data(motionController_, this);
106
+ g_object_remove_weak_pointer(G_OBJECT(motionController_),
107
+ reinterpret_cast<gpointer *>(&motionController_));
108
+ }
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // GTK callbacks
113
+ // ---------------------------------------------------------------------------
114
+
115
+ // GTK counts buttons from 1: 1 is primary, 2 is the wheel, 3 is secondary.
116
+ // Anything above that -- a mouse with side buttons -- is nothing this has an
117
+ // answer for, and treating it as primary would make a thumb button press
118
+ // things.
119
+ basalt::PointerButton buttonOf(GtkGestureClick *gesture) {
120
+ switch (gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture))) {
121
+ case 1:
122
+ return basalt::PointerButton::Primary;
123
+ case 2:
124
+ return basalt::PointerButton::Middle;
125
+ default:
126
+ return basalt::PointerButton::Secondary;
127
+ }
128
+ }
129
+
130
+ void GtkTouchDispatcher::onPressed(GtkGestureClick *gesture,
131
+ int /*count*/,
132
+ double x,
133
+ double y,
134
+ gpointer userData) {
135
+ static_cast<GtkTouchDispatcher *>(userData)->dispatchTouchStart(x, y, buttonOf(gesture));
136
+ }
137
+
138
+ void GtkTouchDispatcher::onReleased(GtkGestureClick *gesture,
139
+ int /*count*/,
140
+ double x,
141
+ double y,
142
+ gpointer userData) {
143
+ static_cast<GtkTouchDispatcher *>(userData)->dispatchTouchEnd(x, y, buttonOf(gesture));
144
+ }
145
+
146
+ void GtkTouchDispatcher::onCancelled(GtkGesture * /*gesture*/,
147
+ GdkEventSequence * /*sequence*/,
148
+ gpointer userData) {
149
+ static_cast<GtkTouchDispatcher *>(userData)->dispatchTouchCancel();
150
+ }
151
+
152
+ void GtkTouchDispatcher::onMotion(GtkEventControllerMotion * /*controller*/,
153
+ double x,
154
+ double y,
155
+ gpointer userData) {
156
+ auto *self = static_cast<GtkTouchDispatcher *>(userData);
157
+ self->dispatchTouchMove(x, y);
158
+ self->dispatchHover(x, y);
159
+ }
160
+
161
+ void GtkTouchDispatcher::onPointerLeft(GtkEventControllerMotion * /*controller*/, gpointer userData) {
162
+ static_cast<GtkTouchDispatcher *>(userData)->dispatchHoverLeave();
163
+ }
164
+
165
+ void GtkTouchDispatcher::synthesiseTap(double x, double y) {
166
+ synthesiseTap(x, y, basalt::PointerButton::Primary);
167
+ }
168
+
169
+ void GtkTouchDispatcher::synthesiseTap(double x, double y, basalt::PointerButton button) {
170
+ dispatchTouchStart(x, y, button);
171
+ dispatchTouchEnd(x, y, button);
172
+ }
173
+
174
+ void GtkTouchDispatcher::synthesiseHover(double x, double y) {
175
+ if (x < 0 || y < 0) {
176
+ dispatchHoverLeave();
177
+ return;
178
+ }
179
+ dispatchHover(x, y);
180
+ }
181
+
182
+ void GtkTouchDispatcher::synthesiseDrag(double fromX, double fromY, double toX, double toY, int steps) {
183
+ dispatchTouchStart(fromX, fromY, basalt::PointerButton::Primary);
184
+ const int count = steps < 1 ? 1 : steps;
185
+ for (int step = 1; step <= count; step++) {
186
+ const double progress = static_cast<double>(step) / count;
187
+ dispatchTouchMove(fromX + (toX - fromX) * progress, fromY + (toY - fromY) * progress);
188
+ }
189
+ dispatchTouchEnd(toX, toY, basalt::PointerButton::Primary);
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Hit testing
194
+ // ---------------------------------------------------------------------------
195
+
196
+ namespace {
197
+
198
+ // `pointerEvents`, which GTK can express one quarter of.
199
+ //
200
+ // `none` is `can-target`, set on the widget when the prop arrives: GTK's own
201
+ // pick then skips the widget and everything inside it, so a press reaches
202
+ // whatever is behind. That is the whole of it and nothing here has to help.
203
+ //
204
+ // The other two have no equivalent, because a GTK widget is targetable or it is
205
+ // not and these are neither:
206
+ //
207
+ // box-only the view is a target and nothing inside it is. Resolved by
208
+ // walking up from the pick: the outermost box-only ancestor is the
209
+ // answer, because everything within it has been taken out of hit
210
+ // testing.
211
+ //
212
+ // box-none the view is not a target and everything inside it is. If the
213
+ // pick landed on the view *itself* then nothing inside it was hit,
214
+ // and the press belongs to whatever is behind -- which is what an
215
+ // absolutely-positioned overlay, the reason this mode exists, is
216
+ // asking for. GTK will answer that question, but only about a
217
+ // widget it considers untargetable, so the view is made so for the
218
+ // length of one more pick and put back.
219
+ //
220
+ // The last part is the only inelegance, and the alternative is worse: a hit
221
+ // test of this project's own over the RnView tree, which would have to redo
222
+ // the transform and clip handling GTK already does correctly. AppKit and Win32
223
+ // own their hit tests and express all three modes directly; this is the same
224
+ // answer reached the only way the toolkit allows.
225
+ RnView *pickTarget(RnView *root, double x, double y) {
226
+ if (root == nullptr) {
227
+ return nullptr;
228
+ }
229
+
230
+ // Bounded: each pass makes exactly one more view untargetable, so a tree of
231
+ // nested box-none views terminates, and a bug here fails a hit test rather
232
+ // than hanging the main loop.
233
+ std::vector<RnView *> suppressed;
234
+ RnView *target = nullptr;
235
+ for (int pass = 0; pass < 16; pass++) {
236
+ GtkWidget *picked = gtk_widget_pick(GTK_WIDGET(root), x, y, GTK_PICK_DEFAULT);
237
+
238
+ // The deepest widget may not be a React Native view -- a <TextInput>'s peer
239
+ // is a GtkText -- and even when every widget is one, walking up is what
240
+ // makes a touch on a child count as a touch on the ancestor that handles
241
+ // it.
242
+ while (picked != nullptr && !RN_IS_VIEW(picked)) {
243
+ picked = gtk_widget_get_parent(picked);
244
+ }
245
+ if (picked == nullptr) {
246
+ break;
247
+ }
248
+
249
+ // Walk to the root, remembering the outermost box-only. It wins over
250
+ // anything found deeper, because nothing deeper is a target at all.
251
+ RnView *boxOnly = nullptr;
252
+ for (GtkWidget *widget = picked; widget != nullptr; widget = gtk_widget_get_parent(widget)) {
253
+ if (RN_IS_VIEW(widget) &&
254
+ rn_view_get_pointer_events(RN_VIEW(widget)) == RN_POINTER_EVENTS_BOX_ONLY) {
255
+ boxOnly = RN_VIEW(widget);
256
+ }
257
+ if (widget == GTK_WIDGET(root)) {
258
+ break;
259
+ }
260
+ }
261
+ if (boxOnly != nullptr) {
262
+ target = boxOnly;
263
+ break;
264
+ }
265
+
266
+ if (rn_view_get_pointer_events(RN_VIEW(picked)) != RN_POINTER_EVENTS_BOX_NONE) {
267
+ target = RN_VIEW(picked);
268
+ break;
269
+ }
270
+ // Transparent, and nothing inside it was hit. Ask again without it.
271
+ gtk_widget_set_can_target(picked, FALSE);
272
+ suppressed.push_back(RN_VIEW(picked));
273
+ }
274
+
275
+ for (RnView *view : suppressed) {
276
+ gtk_widget_set_can_target(GTK_WIDGET(view), TRUE);
277
+ }
278
+ return target;
279
+ }
280
+
281
+ } // namespace
282
+
283
+ Tag hitTestTag(RnView *root, double x, double y) {
284
+ RnView *target = pickTarget(root, x, y);
285
+ return target == nullptr ? 0 : static_cast<Tag>(rn_view_get_tag(target));
286
+ }
287
+
288
+ namespace {
289
+
290
+ // The React Native views under a point, innermost first, each with its origin
291
+ // in the root's coordinates, handed one at a time to `visit`.
292
+ //
293
+ // Two callers want this walk and want different things out of it -- a gesture
294
+ // recogniser wants a HitView, hover wants a HoverView carrying the view's
295
+ // listener mask -- so the walk is written once and the shape of the answer is
296
+ // the caller's business.
297
+ template <typename Visit>
298
+ void walkHitChain(RnView *root, double x, double y, Visit &&visit) {
299
+ if (root == nullptr) {
300
+ return;
301
+ }
302
+
303
+ // From the resolved target rather than from the raw pick, so that a gesture
304
+ // handler and a hover see the same view a press would. A box-only ancestor
305
+ // that swallows a press swallows a hover too.
306
+ GtkWidget *picked = GTK_WIDGET(pickTarget(root, x, y));
307
+ for (GtkWidget *widget = picked; widget != nullptr; widget = gtk_widget_get_parent(widget)) {
308
+ if (RN_IS_VIEW(widget)) {
309
+ // The widget's own origin in the root's coordinates. GTK computes it,
310
+ // rather than this summing frames, so a scrolled ancestor counts.
311
+ graphene_point_t origin{};
312
+ // Not GRAPHENE_POINT_INIT: it is a compound literal, which is C99 rather
313
+ // than C++.
314
+ graphene_point_t zero{};
315
+ zero.x = 0;
316
+ zero.y = 0;
317
+ if (gtk_widget_compute_point(widget, GTK_WIDGET(root), &zero, &origin)) {
318
+ visit(static_cast<int>(rn_view_get_tag(RN_VIEW(widget))), static_cast<double>(origin.x),
319
+ static_cast<double>(origin.y));
320
+ }
321
+ }
322
+ if (widget == GTK_WIDGET(root)) {
323
+ break;
324
+ }
325
+ }
326
+ }
327
+
328
+ // What a gesture recogniser needs and a touch does not: a touch is reported
329
+ // against one target, while a gesture may be attached to any ancestor of the
330
+ // view that was hit.
331
+ //
332
+ // Built only when something is attached; see core/Gestures.h.
333
+ std::vector<basalt::HitView> hitChain(RnView *root, double x, double y) {
334
+ std::vector<basalt::HitView> chain;
335
+ walkHitChain(root, x, y, [&](int tag, double originX, double originY) {
336
+ chain.push_back(basalt::HitView{.tag = tag, .originX = originX, .originY = originY});
337
+ });
338
+ return chain;
339
+ }
340
+
341
+ } // namespace
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // Dispatch
345
+ // ---------------------------------------------------------------------------
346
+
347
+ void GtkTouchDispatcher::dispatchTouchStart(double x, double y, basalt::PointerButton button) {
348
+ // The innermost view, and where inside it the press landed. Needed for the
349
+ // pointer event whichever button it was; PointerEventsProcessor walks up from
350
+ // there itself.
351
+ Tag target = 0;
352
+ double originX = 0;
353
+ double originY = 0;
354
+ walkHitChain(surfaceRoot_, x, y, [&](int tag, double viewX, double viewY) {
355
+ if (target == 0) {
356
+ target = static_cast<Tag>(tag);
357
+ originX = viewX;
358
+ originY = viewY;
359
+ }
360
+ });
361
+
362
+ g_debug("touch start at (%.0f, %.0f) button %d -> tag %d%s",
363
+ x,
364
+ y,
365
+ static_cast<int>(button),
366
+ static_cast<int>(target),
367
+ mountingManager_->eventEmitterForTag(target) != nullptr ? "" : " (no emitter)");
368
+ if (target == 0) {
369
+ return;
370
+ }
371
+
372
+ // Every button produces a pointer event, which is where `button` can be said
373
+ // at all.
374
+ emitPointerButton(true, target, originX, originY, x, y, button);
375
+
376
+ // Only the primary one goes any further. A secondary click is not a press --
377
+ // the web fires no `click` for one and no desktop treats it as an activation
378
+ // -- so it must not reach the responder system, a gesture handler, or
379
+ // `onPress`. See core/PointerButtons.h.
380
+ if (!basalt::isPressButton(button)) {
381
+ return;
382
+ }
383
+
384
+ if (!basalt::gestures().empty()) {
385
+ basalt::gestures().pointerDown(
386
+ hitChain(surfaceRoot_, x, y), x, y, basalt::monotonicMilliseconds());
387
+ }
388
+
389
+ activeTarget_ = target;
390
+ isDown_ = true;
391
+ emit(TouchKind::Start, target, x, y);
392
+
393
+ // A gesture that claimed the pointer on contact -- a native handler -- takes
394
+ // it away from React Native's responder system immediately.
395
+ yieldToGesture(x, y);
396
+ }
397
+
398
+ void GtkTouchDispatcher::dispatchTouchMove(double x, double y) {
399
+ if (!basalt::gestures().empty()) {
400
+ basalt::gestures().pointerMove(x, y, basalt::monotonicMilliseconds());
401
+ }
402
+
403
+ // Motion with no button down is hover, which the touch model has no place
404
+ // for. Reporting it would look to the responder system like a finger dragging
405
+ // across the screen at all times.
406
+ if (!isDown_ || activeTarget_ == 0) {
407
+ return;
408
+ }
409
+ if (yieldToGesture(x, y)) {
410
+ return;
411
+ }
412
+ emit(TouchKind::Move, activeTarget_, x, y);
413
+ }
414
+
415
+ void GtkTouchDispatcher::dispatchTouchEnd(double x, double y, basalt::PointerButton button) {
416
+ // The release half of the pointer event, for every button. Reported against
417
+ // whatever is under the pointer now rather than what was under it on press,
418
+ // which is what a release means when nothing was captured.
419
+ {
420
+ Tag over = 0;
421
+ double originX = 0;
422
+ double originY = 0;
423
+ walkHitChain(surfaceRoot_, x, y, [&](int tag, double viewX, double viewY) {
424
+ if (over == 0) {
425
+ over = static_cast<Tag>(tag);
426
+ originX = viewX;
427
+ originY = viewY;
428
+ }
429
+ });
430
+ if (over != 0) {
431
+ emitPointerButton(false, over, originX, originY, x, y, button);
432
+ }
433
+ }
434
+
435
+ // And nothing else for a button that never pressed anything -- there is no
436
+ // touch to end, no gesture to finish and no <Switch> to toggle.
437
+ if (!basalt::isPressButton(button)) {
438
+ return;
439
+ }
440
+
441
+ if (!basalt::gestures().empty()) {
442
+ basalt::gestures().pointerUp(x, y, basalt::monotonicMilliseconds());
443
+ }
444
+
445
+ if (!isDown_ || activeTarget_ == 0) {
446
+ return;
447
+ }
448
+ const Tag target = activeTarget_;
449
+ isDown_ = false;
450
+ activeTarget_ = 0;
451
+ emit(TouchKind::End, target, x, y);
452
+
453
+ // A <Switch> is toggled from here rather than by the toolkit control, so
454
+ // that a press means the same thing on three desktops -- the Windows one is
455
+ // painted and has no widget to click at all. A no-op for every view that is
456
+ // not a switch.
457
+ if (mountingManager_ != nullptr) {
458
+ mountingManager_->pressedView(target);
459
+ }
460
+ }
461
+
462
+ void GtkTouchDispatcher::dispatchTouchCancel() {
463
+ if (!basalt::gestures().empty()) {
464
+ basalt::gestures().pointerCancel();
465
+ }
466
+
467
+ if (!isDown_ || activeTarget_ == 0) {
468
+ return;
469
+ }
470
+ const Tag target = activeTarget_;
471
+ isDown_ = false;
472
+ activeTarget_ = 0;
473
+ emit(TouchKind::Cancel, target, 0, 0);
474
+ }
475
+
476
+ // ---------------------------------------------------------------------------
477
+ // Hover
478
+ // ---------------------------------------------------------------------------
479
+
480
+ void GtkTouchDispatcher::dispatchHover(double x, double y) {
481
+ // Only the innermost view is reported against -- React Native's
482
+ // PointerEventsProcessor walks up from it itself -- but the whole chain is
483
+ // still needed, because a listener on any ancestor is reason to dispatch.
484
+ int target = 0;
485
+ double originX = 0;
486
+ double originY = 0;
487
+ std::uint16_t listeners = HoverListenerNone;
488
+ walkHitChain(surfaceRoot_, x, y, [&](int tag, double viewX, double viewY) {
489
+ if (target == 0) {
490
+ target = tag;
491
+ originX = viewX;
492
+ originY = viewY;
493
+ }
494
+ listeners |= mountingManager_->hoverListenersForTag(static_cast<Tag>(tag));
495
+ });
496
+
497
+ if (target == 0) {
498
+ dispatchHoverLeave();
499
+ return;
500
+ }
501
+ if (!hover_.admitMove(target, listeners)) {
502
+ return;
503
+ }
504
+ emitPointerMove(target, originX, originY, x, y);
505
+ }
506
+
507
+ void GtkTouchDispatcher::dispatchHoverLeave() {
508
+ const int target = hover_.admitLeave();
509
+ if (target == 0) {
510
+ return;
511
+ }
512
+ const auto emitter = std::dynamic_pointer_cast<const TouchEventEmitter>(
513
+ mountingManager_->eventEmitterForTag(static_cast<Tag>(target)));
514
+ if (emitter == nullptr) {
515
+ return;
516
+ }
517
+ // A leave from a platform is not forwarded: the processor reads it as the
518
+ // pointer being gone and unwinds the path it is holding. So the coordinates
519
+ // on it are never seen by an app, and zero is the honest answer for a
520
+ // position that no longer exists.
521
+ emitter->onPointerLeave(hoverEvent(0, 0, 0, 0));
522
+ }
523
+
524
+ void GtkTouchDispatcher::emitPointerButton(bool down,
525
+ facebook::react::Tag target,
526
+ double originX,
527
+ double originY,
528
+ double x,
529
+ double y,
530
+ basalt::PointerButton button) {
531
+ const auto emitter = std::dynamic_pointer_cast<const TouchEventEmitter>(
532
+ mountingManager_->eventEmitterForTag(target));
533
+ if (emitter == nullptr) {
534
+ return;
535
+ }
536
+ PointerEvent event = hoverEvent(x, y, originX, originY);
537
+ event.button = static_cast<int>(button);
538
+ // Held *during* the event, which is a different number from `button` in the
539
+ // same event: on release nothing is held any more. See core/PointerButtons.h.
540
+ event.buttons = down ? static_cast<int>(basalt::buttonsMaskFor(button)) : 0;
541
+ if (down) {
542
+ emitter->onPointerDown(event);
543
+ } else {
544
+ emitter->onPointerUp(event);
545
+ }
546
+ }
547
+
548
+ void GtkTouchDispatcher::emitPointerMove(
549
+ facebook::react::Tag target, double originX, double originY, double x, double y) {
550
+ const auto emitter = std::dynamic_pointer_cast<const TouchEventEmitter>(
551
+ mountingManager_->eventEmitterForTag(target));
552
+ if (emitter == nullptr) {
553
+ return;
554
+ }
555
+ emitter->onPointerMove(hoverEvent(x, y, originX, originY));
556
+ }
557
+
558
+ // A gesture recogniser that has activated owns the pointer, and React Native's
559
+ // responder system must be told the touch it was following is gone -- otherwise
560
+ // panning across a <Pressable> pans *and* presses it. This is what RNGH's
561
+ // `setJSResponder` does on the platforms it was written for; here both sides
562
+ // are fed from this one place, so it is a cancel rather than a negotiation.
563
+ bool GtkTouchDispatcher::yieldToGesture(double x, double y) {
564
+ if (!isDown_ || activeTarget_ == 0 || !basalt::gestures().hasActiveHandler()) {
565
+ return false;
566
+ }
567
+ const Tag target = activeTarget_;
568
+ isDown_ = false;
569
+ activeTarget_ = 0;
570
+ emit(TouchKind::Cancel, target, x, y);
571
+ return true;
572
+ }
573
+
574
+ // Where a page point falls inside one view, for `Touch::offsetPoint`.
575
+ //
576
+ // Asked of GTK rather than computed here: `gtk_widget_compute_point` walks the
577
+ // widget hierarchy, and this platform composes a view's transform into its
578
+ // allocation -- see docs/DECISIONS.md -- so the answer accounts for a rotated
579
+ // or scaled ancestor without this knowing that transforms exist.
580
+ //
581
+ // The page point is the fallback, which is what every touch carried before. It
582
+ // happens when the target has been unmounted between the press and this event,
583
+ // where nothing better is available and refusing would drop the touch.
584
+ static void pointInView(GtkMountingManager *manager,
585
+ RnView *root,
586
+ Tag target,
587
+ double pageX,
588
+ double pageY,
589
+ double &viewX,
590
+ double &viewY) {
591
+ viewX = pageX;
592
+ viewY = pageY;
593
+
594
+ RnView *view = manager->viewForTag(target);
595
+ if (view == nullptr || root == nullptr) {
596
+ return;
597
+ }
598
+
599
+ // Field by field: GRAPHENE_POINT_INIT is a compound literal, which this
600
+ // build rejects under -Wpedantic.
601
+ graphene_point_t in;
602
+ in.x = static_cast<float>(pageX);
603
+ in.y = static_cast<float>(pageY);
604
+
605
+ graphene_point_t out;
606
+ if (gtk_widget_compute_point(GTK_WIDGET(root), GTK_WIDGET(view), &in, &out)) {
607
+ viewX = out.x;
608
+ viewY = out.y;
609
+ }
610
+ }
611
+
612
+ void GtkTouchDispatcher::emit(TouchKind kind, Tag target, double x, double y) {
613
+ const auto emitter =
614
+ std::dynamic_pointer_cast<const TouchEventEmitter>(mountingManager_->eventEmitterForTag(target));
615
+ if (emitter == nullptr) {
616
+ return;
617
+ }
618
+
619
+ const auto now = HighResTimeStamp::now();
620
+
621
+ Touch touch{};
622
+ touch.identifier = kPointerIdentifier;
623
+ touch.target = target;
624
+ // Coordinates arrive relative to the surface root, which is what React Native
625
+ // calls the page.
626
+ touch.pagePoint = Point{.x = static_cast<facebook::react::Float>(x),
627
+ .y = static_cast<facebook::react::Float>(y)};
628
+ touch.screenPoint = touch.pagePoint;
629
+
630
+ // Relative to the target, which is what `offsetPoint` means and what
631
+ // `locationX`/`locationY` are built from. It carried the page point until
632
+ // now: right only for a view at the surface's origin, and wrong by that
633
+ // view's position for every other.
634
+ double offsetX = x;
635
+ double offsetY = y;
636
+ pointInView(mountingManager_, surfaceRoot_, target, x, y, offsetX, offsetY);
637
+ touch.offsetPoint = Point{.x = static_cast<facebook::react::Float>(offsetX),
638
+ .y = static_cast<facebook::react::Float>(offsetY)};
639
+ touch.force = 1.0F;
640
+ touch.timeStamp = now;
641
+
642
+ Touches changed{};
643
+ changed.insert(touch);
644
+
645
+ const bool isEnd = kind == TouchKind::End || kind == TouchKind::Cancel;
646
+
647
+ TouchEvent event{};
648
+ event.changedTouches = changed;
649
+ // On touchend the finger is gone, so it is no longer in `touches`. Getting
650
+ // this wrong leaves the responder system believing a touch is still active,
651
+ // which swallows the next press.
652
+ event.touches = isEnd ? Touches{} : changed;
653
+ event.targetTouches = event.touches;
654
+
655
+ switch (kind) {
656
+ case TouchKind::Start:
657
+ emitter->onTouchStart(event);
658
+ break;
659
+ case TouchKind::Move:
660
+ emitter->onTouchMove(event);
661
+ break;
662
+ case TouchKind::End:
663
+ emitter->onTouchEnd(event);
664
+ break;
665
+ case TouchKind::Cancel:
666
+ emitter->onTouchCancel(event);
667
+ break;
668
+ }
669
+ }
670
+
671
+ } // namespace basalt