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,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
|