@react-native-gpui/gesture-handler 0.0.0-stage → 3.3.0-alpha.0
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/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +24 -2
- package/desktop/CMakeLists.txt +19 -0
- package/desktop/cpp/GPUIGestureHandlerModule.cpp +500 -0
- package/package.json +38 -4
- package/react-native.config.js +25 -0
- package/src/GestureButtons.desktop.tsx +71 -0
- package/src/NativeRNGestureHandlerModule.ts +32 -0
- package/src/customDirectEventTypes.desktop.ts +13 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@react-native-gpui/gesture-handler` are listed here. Each version's section becomes the notes of its GitHub release.
|
|
4
|
+
|
|
5
|
+
## 3.3.0-alpha.0 - 2026-11-01
|
|
6
|
+
|
|
7
|
+
- First release, for `react-native-gesture-handler` 3.3.x with Reanimated 4.7.x and Worklets 0.13.x.
|
|
8
|
+
- Gesture recognition for mouse, trackpad and touch input on desktop.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Streetwriters
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @react-native-gpui/gesture-handler
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Desktop support for [`react-native-gesture-handler`](https://github.com/software-mansion/react-native-gesture-handler): gestures from the mouse, trackpad and touch screen.
|
|
4
|
+
|
|
5
|
+
Part of [react-native-gpui](https://github.com/streetwriters/react-native-gpui), React Native for macOS, Windows and Linux.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install it next to `react-native-gesture-handler`:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install react-native-gesture-handler@3.3.x @react-native-gpui/gesture-handler
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Gesture Handler needs Reanimated 4.7.x and Worklets 0.13.x, with their desktop packages [`@react-native-gpui/reanimated`](https://www.npmjs.com/package/@react-native-gpui/reanimated) and [`@react-native-gpui/worklets`](https://www.npmjs.com/package/@react-native-gpui/worklets).
|
|
16
|
+
|
|
17
|
+
Keep using `react-native-gesture-handler` as you do on iOS and Android; its JavaScript API is unchanged. Autolinking builds the desktop side when the app is built.
|
|
18
|
+
|
|
19
|
+
## Documentation
|
|
20
|
+
|
|
21
|
+
[Supported libraries](https://streetwriters.github.io/react-native-gpui/docs/supported-libraries)
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
if(NOT RN_GPUI_PROJECT_ROOT)
|
|
2
|
+
message(FATAL_ERROR
|
|
3
|
+
"@react-native-gpui/gesture-handler was added by the platform; RN_GPUI_PROJECT_ROOT must be set by its build.rs")
|
|
4
|
+
endif()
|
|
5
|
+
rn_gpui_find_node_module(react-native-gesture-handler _gesture_root)
|
|
6
|
+
if(NOT EXISTS "${_gesture_root}/src/specs/NativeRNGestureHandlerModule.ts")
|
|
7
|
+
message(FATAL_ERROR "react-native-gesture-handler 3.3.x was not found at ${_gesture_root}")
|
|
8
|
+
endif()
|
|
9
|
+
|
|
10
|
+
file(READ "${_gesture_root}/package.json" _gesture_package_json)
|
|
11
|
+
string(JSON _gesture_version GET "${_gesture_package_json}" version)
|
|
12
|
+
if(NOT _gesture_version MATCHES "^3\\.3\\.")
|
|
13
|
+
message(FATAL_ERROR "@react-native-gpui/gesture-handler supports react-native-gesture-handler 3.3.x, found ${_gesture_version}")
|
|
14
|
+
endif()
|
|
15
|
+
|
|
16
|
+
set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_SOURCES
|
|
17
|
+
"${CMAKE_CURRENT_LIST_DIR}/cpp/GPUIGestureHandlerModule.cpp")
|
|
18
|
+
set_property(GLOBAL APPEND PROPERTY RN_GPUI_APP_CPP_INCLUDE_DIRS
|
|
19
|
+
"${RN_GPUI_SYS_CPP_DIR}")
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#include "GpuiHost.h"
|
|
2
|
+
#include "GpuiModules.h"
|
|
3
|
+
|
|
4
|
+
#include <GPUIGestureHandlerSpec.h>
|
|
5
|
+
#include <ReactCommon/TurboModule.h>
|
|
6
|
+
#include <folly/dynamic.h>
|
|
7
|
+
#include <folly/json.h>
|
|
8
|
+
#include <glog/logging.h>
|
|
9
|
+
|
|
10
|
+
#include <algorithm>
|
|
11
|
+
#include <cmath>
|
|
12
|
+
#include <cstdlib>
|
|
13
|
+
#include <memory>
|
|
14
|
+
#include <mutex>
|
|
15
|
+
#include <string>
|
|
16
|
+
#include <unordered_map>
|
|
17
|
+
#include <utility>
|
|
18
|
+
#include <vector>
|
|
19
|
+
|
|
20
|
+
namespace {
|
|
21
|
+
using facebook::jsi::Runtime;
|
|
22
|
+
using facebook::react::TurboModule;
|
|
23
|
+
using rngpui::extension::RawPointerEvent;
|
|
24
|
+
using rngpui::extension::RecognizedGesture;
|
|
25
|
+
|
|
26
|
+
constexpr int32_t kUndetermined = 0;
|
|
27
|
+
constexpr int32_t kFailed = 1;
|
|
28
|
+
constexpr int32_t kBegan = 2;
|
|
29
|
+
constexpr int32_t kCancelled = 3;
|
|
30
|
+
constexpr int32_t kActive = 4;
|
|
31
|
+
constexpr int32_t kEnd = 5;
|
|
32
|
+
|
|
33
|
+
struct Handler {
|
|
34
|
+
std::string name;
|
|
35
|
+
folly::dynamic config{folly::dynamic::object};
|
|
36
|
+
int32_t viewTag{-1};
|
|
37
|
+
int32_t actionType{0};
|
|
38
|
+
int32_t state{kUndetermined};
|
|
39
|
+
float startX{0};
|
|
40
|
+
float startY{0};
|
|
41
|
+
float lastX{0};
|
|
42
|
+
float lastY{0};
|
|
43
|
+
double startTime{0};
|
|
44
|
+
double lastTime{0};
|
|
45
|
+
bool recognized{false};
|
|
46
|
+
std::vector<int32_t> simultaneousHandlers;
|
|
47
|
+
std::vector<int32_t> waitFor;
|
|
48
|
+
std::vector<int32_t> blocksHandlers;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
std::mutex handlersMutex;
|
|
52
|
+
std::unordered_map<int32_t, Handler> handlers;
|
|
53
|
+
|
|
54
|
+
folly::dynamic configObject(folly::dynamic config) {
|
|
55
|
+
return config.isObject() ? std::move(config) : folly::dynamic::object();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
float number(const folly::dynamic& object, const char* key, float fallback) {
|
|
59
|
+
if (!object.isObject() || !object.get_ptr(key))
|
|
60
|
+
return fallback;
|
|
61
|
+
const auto& value = object[key];
|
|
62
|
+
if (value.isNumber())
|
|
63
|
+
return static_cast<float>(value.asDouble());
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
bool enabled(const Handler& handler) {
|
|
68
|
+
const auto* value = handler.config.get_ptr("enabled");
|
|
69
|
+
return value == nullptr || !value->isBool() || value->asBool();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
bool isPan(const Handler& handler) {
|
|
73
|
+
return handler.name == "Pan" || handler.name == "PanGestureHandler";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
bool isTap(const Handler& handler) {
|
|
77
|
+
return handler.name == "Tap" || handler.name == "TapGestureHandler";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
bool isLongPress(const Handler& handler) {
|
|
81
|
+
return handler.name == "LongPress" ||
|
|
82
|
+
handler.name == "LongPressGestureHandler";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
bool gestureTracingEnabled() {
|
|
86
|
+
return std::getenv("RN_GPUI_DEBUG_GESTURES") != nullptr;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
folly::dynamic payloadFor(
|
|
90
|
+
int32_t handlerTag,
|
|
91
|
+
const Handler& handler,
|
|
92
|
+
int32_t state,
|
|
93
|
+
int32_t oldState,
|
|
94
|
+
const RawPointerEvent& pointer,
|
|
95
|
+
bool stateChange) {
|
|
96
|
+
const float dx = pointer.x - handler.startX;
|
|
97
|
+
const float dy = pointer.y - handler.startY;
|
|
98
|
+
const double elapsed = std::max(1.0, pointer.timestamp - handler.lastTime);
|
|
99
|
+
folly::dynamic payload = folly::dynamic::object("handlerTag", handlerTag)(
|
|
100
|
+
"state", state)("x", pointer.x - handler.startX)(
|
|
101
|
+
"y", pointer.y - handler.startY)("absoluteX", pointer.x)(
|
|
102
|
+
"absoluteY", pointer.y)("translationX", dx)("translationY", dy)(
|
|
103
|
+
"velocityX", (pointer.x - handler.lastX) * 1000.0 / elapsed)(
|
|
104
|
+
"velocityY", (pointer.y - handler.lastY) * 1000.0 / elapsed)(
|
|
105
|
+
"numberOfPointers", 1)("pointerType", 2)(
|
|
106
|
+
"duration", pointer.timestamp - handler.startTime);
|
|
107
|
+
if (stateChange) {
|
|
108
|
+
payload["oldState"] = oldState;
|
|
109
|
+
}
|
|
110
|
+
return payload;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
void appendEvent(
|
|
114
|
+
std::vector<RecognizedGesture>& events,
|
|
115
|
+
int32_t viewTag,
|
|
116
|
+
const char* type,
|
|
117
|
+
const folly::dynamic& payload) {
|
|
118
|
+
events.push_back({viewTag, type, folly::toJson(payload)});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
void appendState(
|
|
122
|
+
std::vector<RecognizedGesture>& events,
|
|
123
|
+
int32_t handlerTag,
|
|
124
|
+
Handler& handler,
|
|
125
|
+
int32_t nextState,
|
|
126
|
+
const RawPointerEvent& pointer) {
|
|
127
|
+
if (handler.state == nextState)
|
|
128
|
+
return;
|
|
129
|
+
const auto oldState = handler.state;
|
|
130
|
+
handler.state = nextState;
|
|
131
|
+
if (nextState == kActive) {
|
|
132
|
+
handler.recognized = true;
|
|
133
|
+
}
|
|
134
|
+
appendEvent(
|
|
135
|
+
events,
|
|
136
|
+
handler.viewTag,
|
|
137
|
+
"topGestureHandlerStateChange",
|
|
138
|
+
payloadFor(handlerTag, handler, nextState, oldState, pointer, true));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
std::vector<int32_t> relationTags(
|
|
142
|
+
const folly::dynamic& relations,
|
|
143
|
+
const char* key) {
|
|
144
|
+
std::vector<int32_t> tags;
|
|
145
|
+
const auto* values = relations.get_ptr(key);
|
|
146
|
+
if (values == nullptr || !values->isArray()) {
|
|
147
|
+
return tags;
|
|
148
|
+
}
|
|
149
|
+
tags.reserve(values->size());
|
|
150
|
+
for (const auto& value : *values) {
|
|
151
|
+
if (value.isInt()) {
|
|
152
|
+
tags.push_back(static_cast<int32_t>(value.asInt()));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return tags;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
bool hasTag(const std::vector<int32_t>& tags, int32_t tag) {
|
|
159
|
+
return std::find(tags.begin(), tags.end(), tag) != tags.end();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
bool hasRecognizedWaitFor(const Handler& handler) {
|
|
163
|
+
for (const auto tag : handler.waitFor) {
|
|
164
|
+
const auto dependency = handlers.find(tag);
|
|
165
|
+
if (dependency != handlers.end() && dependency->second.recognized) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
bool hasUnresolvedWaitFor(const Handler& handler) {
|
|
173
|
+
for (const auto tag : handler.waitFor) {
|
|
174
|
+
const auto dependency = handlers.find(tag);
|
|
175
|
+
if (dependency != handlers.end() && dependency->second.state == kBegan &&
|
|
176
|
+
!dependency->second.recognized) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
bool blockedByActiveHandler(int32_t handlerTag) {
|
|
184
|
+
for (const auto& [otherTag, other] : handlers) {
|
|
185
|
+
if (otherTag != handlerTag && other.state == kActive &&
|
|
186
|
+
hasTag(other.blocksHandlers, handlerTag)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
std::vector<RecognizedGesture> processPointer(const RawPointerEvent& pointer) {
|
|
194
|
+
std::vector<RecognizedGesture> events;
|
|
195
|
+
std::lock_guard<std::mutex> lock(handlersMutex);
|
|
196
|
+
if (gestureTracingEnabled() && pointer.phase != 4) {
|
|
197
|
+
LOG(INFO) << "[gesture-handler] pointer phase=" << pointer.phase << " at ("
|
|
198
|
+
<< pointer.x << ',' << pointer.y
|
|
199
|
+
<< ") chain=" << pointer.targetChain.size() << " tags";
|
|
200
|
+
}
|
|
201
|
+
if (pointer.phase == 0) {
|
|
202
|
+
for (auto& [_, handler] : handlers) {
|
|
203
|
+
if (!enabled(handler) || handler.viewTag < 0 ||
|
|
204
|
+
std::find(
|
|
205
|
+
pointer.targetChain.begin(),
|
|
206
|
+
pointer.targetChain.end(),
|
|
207
|
+
handler.viewTag) == pointer.targetChain.end()) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
handler.state = kUndetermined;
|
|
211
|
+
handler.startX = pointer.x;
|
|
212
|
+
handler.startY = pointer.y;
|
|
213
|
+
handler.lastX = pointer.x;
|
|
214
|
+
handler.lastY = pointer.y;
|
|
215
|
+
handler.startTime = pointer.timestamp;
|
|
216
|
+
handler.lastTime = pointer.timestamp;
|
|
217
|
+
handler.recognized = false;
|
|
218
|
+
appendState(events, _, handler, kBegan, pointer);
|
|
219
|
+
if (gestureTracingEnabled()) {
|
|
220
|
+
LOG(INFO) << "[gesture-handler] began handler=" << _
|
|
221
|
+
<< " name=" << handler.name << " view=" << handler.viewTag;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return events;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (pointer.phase == 2) {
|
|
228
|
+
// Resolve recognizers that cannot succeed on release before testing tap
|
|
229
|
+
// handlers that may be waiting for them to fail.
|
|
230
|
+
for (auto& [handlerTag, handler] : handlers) {
|
|
231
|
+
if (handler.state != kBegan || isTap(handler)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const float dx = pointer.x - handler.startX;
|
|
235
|
+
const float dy = pointer.y - handler.startY;
|
|
236
|
+
if (isLongPress(handler)) {
|
|
237
|
+
const auto elapsed = pointer.timestamp - handler.startTime;
|
|
238
|
+
const auto minDuration =
|
|
239
|
+
number(handler.config, "minDurationMs", 500.0f);
|
|
240
|
+
if (elapsed >= minDuration &&
|
|
241
|
+
std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f) &&
|
|
242
|
+
!hasRecognizedWaitFor(handler) &&
|
|
243
|
+
!blockedByActiveHandler(handlerTag)) {
|
|
244
|
+
appendState(events, handlerTag, handler, kActive, pointer);
|
|
245
|
+
} else {
|
|
246
|
+
appendState(events, handlerTag, handler, kFailed, pointer);
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
appendState(events, handlerTag, handler, kFailed, pointer);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (auto& [handlerTag, handler] : handlers) {
|
|
255
|
+
if (handler.state != kBegan && handler.state != kActive)
|
|
256
|
+
continue;
|
|
257
|
+
if (pointer.phase == 4) {
|
|
258
|
+
const float dx = pointer.x - handler.startX;
|
|
259
|
+
const float dy = pointer.y - handler.startY;
|
|
260
|
+
const auto elapsed = pointer.timestamp - handler.startTime;
|
|
261
|
+
const auto minDuration = number(handler.config, "minDurationMs", 500.0f);
|
|
262
|
+
if (gestureTracingEnabled() && handler.state == kBegan &&
|
|
263
|
+
isLongPress(handler) && elapsed >= minDuration) {
|
|
264
|
+
LOG(INFO) << "[gesture-handler] long-press threshold handler="
|
|
265
|
+
<< handlerTag << " elapsed=" << elapsed
|
|
266
|
+
<< " required=" << minDuration
|
|
267
|
+
<< " distance=" << std::hypot(dx, dy);
|
|
268
|
+
}
|
|
269
|
+
if (handler.state == kBegan && isLongPress(handler) &&
|
|
270
|
+
elapsed >= minDuration &&
|
|
271
|
+
std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f)) {
|
|
272
|
+
if (hasRecognizedWaitFor(handler) ||
|
|
273
|
+
blockedByActiveHandler(handlerTag)) {
|
|
274
|
+
appendState(events, handlerTag, handler, kFailed, pointer);
|
|
275
|
+
} else if (!hasUnresolvedWaitFor(handler)) {
|
|
276
|
+
appendState(events, handlerTag, handler, kActive, pointer);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} else if (pointer.phase == 1) {
|
|
280
|
+
const float dx = pointer.x - handler.startX;
|
|
281
|
+
const float dy = pointer.y - handler.startY;
|
|
282
|
+
if (handler.state == kBegan && isPan(handler) &&
|
|
283
|
+
std::hypot(dx, dy) >= number(handler.config, "minDist", 3.0f)) {
|
|
284
|
+
if (hasRecognizedWaitFor(handler) ||
|
|
285
|
+
blockedByActiveHandler(handlerTag)) {
|
|
286
|
+
appendState(events, handlerTag, handler, kFailed, pointer);
|
|
287
|
+
} else if (!hasUnresolvedWaitFor(handler)) {
|
|
288
|
+
appendState(events, handlerTag, handler, kActive, pointer);
|
|
289
|
+
}
|
|
290
|
+
} else if (
|
|
291
|
+
handler.state == kBegan && isLongPress(handler) &&
|
|
292
|
+
pointer.timestamp - handler.startTime >=
|
|
293
|
+
number(handler.config, "minDurationMs", 500.0f) &&
|
|
294
|
+
std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f)) {
|
|
295
|
+
if (hasRecognizedWaitFor(handler) ||
|
|
296
|
+
blockedByActiveHandler(handlerTag)) {
|
|
297
|
+
appendState(events, handlerTag, handler, kFailed, pointer);
|
|
298
|
+
} else if (!hasUnresolvedWaitFor(handler)) {
|
|
299
|
+
appendState(events, handlerTag, handler, kActive, pointer);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (handler.state == kActive &&
|
|
303
|
+
(isPan(handler) || isLongPress(handler))) {
|
|
304
|
+
appendEvent(
|
|
305
|
+
events,
|
|
306
|
+
handler.viewTag,
|
|
307
|
+
"topGestureHandlerEvent",
|
|
308
|
+
payloadFor(
|
|
309
|
+
handlerTag,
|
|
310
|
+
handler,
|
|
311
|
+
handler.state,
|
|
312
|
+
kUndetermined,
|
|
313
|
+
pointer,
|
|
314
|
+
false));
|
|
315
|
+
}
|
|
316
|
+
} else if (pointer.phase == 2) {
|
|
317
|
+
const float dx = pointer.x - handler.startX;
|
|
318
|
+
const float dy = pointer.y - handler.startY;
|
|
319
|
+
if (isTap(handler) && handler.state == kBegan &&
|
|
320
|
+
pointer.timestamp - handler.startTime <=
|
|
321
|
+
number(handler.config, "maxDurationMs", 500.0f) &&
|
|
322
|
+
std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f)) {
|
|
323
|
+
if (hasRecognizedWaitFor(handler) ||
|
|
324
|
+
blockedByActiveHandler(handlerTag)) {
|
|
325
|
+
appendState(events, handlerTag, handler, kFailed, pointer);
|
|
326
|
+
} else if (!hasUnresolvedWaitFor(handler)) {
|
|
327
|
+
appendState(events, handlerTag, handler, kActive, pointer);
|
|
328
|
+
} else {
|
|
329
|
+
appendState(events, handlerTag, handler, kFailed, pointer);
|
|
330
|
+
}
|
|
331
|
+
} else if (
|
|
332
|
+
isLongPress(handler) && handler.state == kBegan &&
|
|
333
|
+
pointer.timestamp - handler.startTime >=
|
|
334
|
+
number(handler.config, "minDurationMs", 500.0f) &&
|
|
335
|
+
std::hypot(dx, dy) <= number(handler.config, "maxDist", 10.0f)) {
|
|
336
|
+
appendState(events, handlerTag, handler, kActive, pointer);
|
|
337
|
+
} else if (handler.state == kBegan) {
|
|
338
|
+
appendState(events, handlerTag, handler, kFailed, pointer);
|
|
339
|
+
}
|
|
340
|
+
if (handler.state == kActive) {
|
|
341
|
+
if (isTap(handler) || isLongPress(handler)) {
|
|
342
|
+
appendEvent(
|
|
343
|
+
events,
|
|
344
|
+
handler.viewTag,
|
|
345
|
+
"topGestureHandlerEvent",
|
|
346
|
+
payloadFor(
|
|
347
|
+
handlerTag,
|
|
348
|
+
handler,
|
|
349
|
+
handler.state,
|
|
350
|
+
kUndetermined,
|
|
351
|
+
pointer,
|
|
352
|
+
false));
|
|
353
|
+
}
|
|
354
|
+
appendState(events, handlerTag, handler, kEnd, pointer);
|
|
355
|
+
}
|
|
356
|
+
handler.lastX = pointer.x;
|
|
357
|
+
handler.lastY = pointer.y;
|
|
358
|
+
handler.lastTime = pointer.timestamp;
|
|
359
|
+
} else if (pointer.phase == 3) {
|
|
360
|
+
appendState(
|
|
361
|
+
events,
|
|
362
|
+
handlerTag,
|
|
363
|
+
handler,
|
|
364
|
+
handler.state == kActive ? kCancelled : kFailed,
|
|
365
|
+
pointer);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (gestureTracingEnabled() && (pointer.phase != 4 || !events.empty())) {
|
|
369
|
+
LOG(INFO) << "[gesture-handler] recognized events=" << events.size();
|
|
370
|
+
}
|
|
371
|
+
return events;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
class GPUIGestureHandlerModule final
|
|
375
|
+
: public facebook::react::NativeRNGestureHandlerModuleSpec<
|
|
376
|
+
GPUIGestureHandlerModule> {
|
|
377
|
+
public:
|
|
378
|
+
explicit GPUIGestureHandlerModule(
|
|
379
|
+
std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
|
|
380
|
+
: NativeRNGestureHandlerModuleSpec(std::move(jsInvoker)) {}
|
|
381
|
+
|
|
382
|
+
// The JS side queues operations and flushes them in order, so every method
|
|
383
|
+
// has to observe the same order the JS thread produced.
|
|
384
|
+
static constexpr rngpui::ModuleThread kThread = rngpui::ModuleThread::JS;
|
|
385
|
+
|
|
386
|
+
void createGestureHandler(
|
|
387
|
+
Runtime&,
|
|
388
|
+
std::string handlerName,
|
|
389
|
+
double handlerTag,
|
|
390
|
+
folly::dynamic config) {
|
|
391
|
+
Handler handler;
|
|
392
|
+
handler.name = std::move(handlerName);
|
|
393
|
+
handler.config = configObject(std::move(config));
|
|
394
|
+
std::lock_guard<std::mutex> lock(handlersMutex);
|
|
395
|
+
handlers.insert_or_assign(
|
|
396
|
+
static_cast<int32_t>(handlerTag), std::move(handler));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
void attachGestureHandler(
|
|
400
|
+
Runtime&,
|
|
401
|
+
double handlerTag,
|
|
402
|
+
double viewTag,
|
|
403
|
+
double actionType) {
|
|
404
|
+
const auto tag = static_cast<int32_t>(handlerTag);
|
|
405
|
+
std::lock_guard<std::mutex> lock(handlersMutex);
|
|
406
|
+
auto found = handlers.find(tag);
|
|
407
|
+
if (found != handlers.end()) {
|
|
408
|
+
found->second.viewTag = static_cast<int32_t>(viewTag);
|
|
409
|
+
found->second.actionType = static_cast<int32_t>(actionType);
|
|
410
|
+
if (gestureTracingEnabled()) {
|
|
411
|
+
LOG(INFO) << "[gesture-handler] attached handler=" << tag
|
|
412
|
+
<< " view=" << found->second.viewTag
|
|
413
|
+
<< " action=" << found->second.actionType;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
void setGestureHandlerConfig(
|
|
419
|
+
Runtime&,
|
|
420
|
+
double handlerTag,
|
|
421
|
+
folly::dynamic newConfig) {
|
|
422
|
+
const auto tag = static_cast<int32_t>(handlerTag);
|
|
423
|
+
auto config = configObject(std::move(newConfig));
|
|
424
|
+
std::lock_guard<std::mutex> lock(handlersMutex);
|
|
425
|
+
auto found = handlers.find(tag);
|
|
426
|
+
if (found != handlers.end())
|
|
427
|
+
found->second.config = std::move(config);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
void updateGestureHandlerConfig(
|
|
431
|
+
Runtime&,
|
|
432
|
+
double handlerTag,
|
|
433
|
+
folly::dynamic newConfig) {
|
|
434
|
+
const auto tag = static_cast<int32_t>(handlerTag);
|
|
435
|
+
std::lock_guard<std::mutex> lock(handlersMutex);
|
|
436
|
+
auto found = handlers.find(tag);
|
|
437
|
+
if (found != handlers.end() && newConfig.isObject()) {
|
|
438
|
+
for (const auto& [key, value] : newConfig.items())
|
|
439
|
+
found->second.config[key] = value;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
void
|
|
444
|
+
configureRelations(Runtime&, double handlerTag, folly::dynamic relations) {
|
|
445
|
+
const auto tag = static_cast<int32_t>(handlerTag);
|
|
446
|
+
std::lock_guard<std::mutex> lock(handlersMutex);
|
|
447
|
+
const auto found = handlers.find(tag);
|
|
448
|
+
if (found != handlers.end()) {
|
|
449
|
+
found->second.simultaneousHandlers =
|
|
450
|
+
relationTags(relations, "simultaneousHandlers");
|
|
451
|
+
found->second.waitFor = relationTags(relations, "waitFor");
|
|
452
|
+
found->second.blocksHandlers = relationTags(relations, "blocksHandlers");
|
|
453
|
+
if (gestureTracingEnabled()) {
|
|
454
|
+
auto formatTags = [](const std::vector<int32_t>& tags) {
|
|
455
|
+
std::string result;
|
|
456
|
+
for (const auto value : tags) {
|
|
457
|
+
if (!result.empty())
|
|
458
|
+
result += ",";
|
|
459
|
+
result += std::to_string(value);
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
};
|
|
463
|
+
LOG(INFO) << "[gesture-handler] relations handler=" << tag
|
|
464
|
+
<< " name=" << found->second.name << " simultaneous="
|
|
465
|
+
<< formatTags(found->second.simultaneousHandlers)
|
|
466
|
+
<< " waitFor=" << formatTags(found->second.waitFor)
|
|
467
|
+
<< " blocks=" << formatTags(found->second.blocksHandlers);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
void dropGestureHandler(Runtime&, double handlerTag) {
|
|
473
|
+
std::lock_guard<std::mutex> lock(handlersMutex);
|
|
474
|
+
handlers.erase(static_cast<int32_t>(handlerTag));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
void flushOperations(Runtime&) {}
|
|
478
|
+
|
|
479
|
+
bool installUIRuntimeBindings(Runtime&) {
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
} // namespace
|
|
484
|
+
|
|
485
|
+
std::shared_ptr<facebook::react::TurboModule>
|
|
486
|
+
create_gpui_gesture_handler_module(
|
|
487
|
+
const std::string& name,
|
|
488
|
+
std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
|
|
489
|
+
if (name != GPUIGestureHandlerModule::kModuleName) {
|
|
490
|
+
return nullptr;
|
|
491
|
+
}
|
|
492
|
+
rngpui::registerPointerObserver(
|
|
493
|
+
[](const rngpui::extension::RawPointerEvent& pointer) {
|
|
494
|
+
return processPointer(pointer);
|
|
495
|
+
});
|
|
496
|
+
if (gestureTracingEnabled()) {
|
|
497
|
+
LOG(INFO) << "[gesture-handler] native pointer processor installed";
|
|
498
|
+
}
|
|
499
|
+
return std::make_shared<GPUIGestureHandlerModule>(std::move(jsInvoker));
|
|
500
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-native-gpui/gesture-handler",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "3.3.0-alpha.0",
|
|
4
|
+
"description": "Native GPUI gesture recognition for react-native-gesture-handler",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/streetwriters/react-native-gpui.git",
|
|
9
|
+
"directory": "modules/gesture-handler"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://streetwriters.github.io/react-native-gpui/",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/streetwriters/react-native-gpui/issues"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"desktop",
|
|
17
|
+
"react-native.config.js",
|
|
18
|
+
"src",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"!**/*.test.js",
|
|
21
|
+
"!**/*.test.mjs",
|
|
22
|
+
"!**/*.test.ts",
|
|
23
|
+
"!**/*.test.cjs"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react-native": "0.87.x",
|
|
27
|
+
"react-native-gesture-handler": "3.3.x",
|
|
28
|
+
"react-native-reanimated": "4.7.x",
|
|
29
|
+
"react-native-worklets": "0.13.x"
|
|
30
|
+
},
|
|
31
|
+
"codegenConfig": {
|
|
32
|
+
"name": "GPUIGestureHandlerSpec",
|
|
33
|
+
"type": "modules",
|
|
34
|
+
"jsSrcsDir": "src"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"tag": "latest"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
dependency: {
|
|
3
|
+
platforms: {
|
|
4
|
+
desktop: {
|
|
5
|
+
cmake: 'desktop',
|
|
6
|
+
cppProvider: 'create_gpui_gesture_handler_module',
|
|
7
|
+
componentCodegenPackage: 'react-native-gesture-handler',
|
|
8
|
+
componentDescriptors: [
|
|
9
|
+
'RNGestureHandlerButtonComponentDescriptor',
|
|
10
|
+
'RNGestureHandlerRootViewComponentDescriptor',
|
|
11
|
+
],
|
|
12
|
+
componentHeaders: [
|
|
13
|
+
'react/renderer/components/rngesturehandler_codegen/ComponentDescriptors.h',
|
|
14
|
+
],
|
|
15
|
+
// Metro: replace these files of installed packages for gpui.
|
|
16
|
+
sourceOverrides: {
|
|
17
|
+
'react-native-gesture-handler/src/handlers/customDirectEventTypes.ts':
|
|
18
|
+
'./src/customDirectEventTypes.desktop.ts',
|
|
19
|
+
'react-native-gesture-handler/src/v3/components/GestureButtons.tsx':
|
|
20
|
+
'./src/GestureButtons.desktop.tsx',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './customDirectEventTypes.desktop';
|
|
3
|
+
import {
|
|
4
|
+
Pressable,
|
|
5
|
+
type ColorValue,
|
|
6
|
+
type PressableProps,
|
|
7
|
+
type StyleProp,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
type DesktopButtonProps = Omit<PressableProps, 'style'> & {
|
|
12
|
+
style?: StyleProp<ViewStyle>;
|
|
13
|
+
activeOpacity?: number;
|
|
14
|
+
underlayColor?: ColorValue;
|
|
15
|
+
onActiveStateChange?: (active: boolean) => void;
|
|
16
|
+
pressedStyle?: ViewStyle;
|
|
17
|
+
borderless?: boolean;
|
|
18
|
+
rippleColor?: ColorValue | number | null;
|
|
19
|
+
rippleRadius?: number | null;
|
|
20
|
+
foreground?: boolean;
|
|
21
|
+
exclusive?: boolean;
|
|
22
|
+
cancelable?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function DesktopButton({
|
|
26
|
+
activeOpacity = 0.75,
|
|
27
|
+
onActiveStateChange,
|
|
28
|
+
onPressIn,
|
|
29
|
+
onPressOut,
|
|
30
|
+
pressedStyle,
|
|
31
|
+
style,
|
|
32
|
+
...props
|
|
33
|
+
}: DesktopButtonProps) {
|
|
34
|
+
return (
|
|
35
|
+
<Pressable
|
|
36
|
+
{...props}
|
|
37
|
+
style={({ pressed }) => [
|
|
38
|
+
style,
|
|
39
|
+
pressed ? [{ opacity: activeOpacity }, pressedStyle] : undefined,
|
|
40
|
+
]}
|
|
41
|
+
onPressIn={event => {
|
|
42
|
+
onActiveStateChange?.(true);
|
|
43
|
+
onPressIn?.(event);
|
|
44
|
+
}}
|
|
45
|
+
onPressOut={event => {
|
|
46
|
+
onActiveStateChange?.(false);
|
|
47
|
+
onPressOut?.(event);
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const RectButton = ({ underlayColor, ...props }: DesktopButtonProps) => (
|
|
54
|
+
<DesktopButton
|
|
55
|
+
{...props}
|
|
56
|
+
pressedStyle={
|
|
57
|
+
underlayColor == null ? undefined : { backgroundColor: underlayColor }
|
|
58
|
+
}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export const BorderlessButton = (props: DesktopButtonProps) => (
|
|
63
|
+
<DesktopButton {...props} />
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export const RawButton = DesktopButton;
|
|
67
|
+
export const BaseButton = DesktopButton;
|
|
68
|
+
export const LegacyRectButton = RectButton;
|
|
69
|
+
export const LegacyBorderlessButton = BorderlessButton;
|
|
70
|
+
export const LegacyRawButton = RawButton;
|
|
71
|
+
export const LegacyBaseButton = BaseButton;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CodegenTypes, TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface Spec extends TurboModule {
|
|
5
|
+
createGestureHandler: (
|
|
6
|
+
handlerName: string,
|
|
7
|
+
handlerTag: CodegenTypes.Double,
|
|
8
|
+
config: Object,
|
|
9
|
+
) => void;
|
|
10
|
+
attachGestureHandler: (
|
|
11
|
+
handlerTag: CodegenTypes.Double,
|
|
12
|
+
viewTag: CodegenTypes.Double,
|
|
13
|
+
actionType: CodegenTypes.Double,
|
|
14
|
+
) => void;
|
|
15
|
+
setGestureHandlerConfig: (
|
|
16
|
+
handlerTag: CodegenTypes.Double,
|
|
17
|
+
newConfig: Object,
|
|
18
|
+
) => void;
|
|
19
|
+
updateGestureHandlerConfig: (
|
|
20
|
+
handlerTag: CodegenTypes.Double,
|
|
21
|
+
newConfig: Object,
|
|
22
|
+
) => void;
|
|
23
|
+
configureRelations: (
|
|
24
|
+
handlerTag: CodegenTypes.Double,
|
|
25
|
+
relations: Object,
|
|
26
|
+
) => void;
|
|
27
|
+
dropGestureHandler: (handlerTag: CodegenTypes.Double) => void;
|
|
28
|
+
flushOperations: () => void;
|
|
29
|
+
installUIRuntimeBindings: () => boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('RNGestureHandlerModule');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// RNGH's legacy handler path registers only the continuous event in its JS
|
|
2
|
+
// module. GPUI dispatches the codegen state-change event directly, so expose
|
|
3
|
+
// both direct-event registrations from this platform package.
|
|
4
|
+
import { customDirectEventTypes } from 'react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry';
|
|
5
|
+
|
|
6
|
+
customDirectEventTypes.topGestureHandlerEvent = {
|
|
7
|
+
registrationName: 'onGestureHandlerEvent',
|
|
8
|
+
};
|
|
9
|
+
customDirectEventTypes.topGestureHandlerStateChange = {
|
|
10
|
+
registrationName: 'onGestureHandlerStateChange',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { customDirectEventTypes };
|