basalt-appkit 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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runMacos.js +33 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +298 -0
  5. package/native/appkit/AppKitAnimationChoreographer.h +54 -0
  6. package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
  7. package/native/appkit/AppKitDropTarget.h +34 -0
  8. package/native/appkit/AppKitDropTarget.mm +292 -0
  9. package/native/appkit/AppKitFocus.h +120 -0
  10. package/native/appkit/AppKitFocus.mm +293 -0
  11. package/native/appkit/AppKitImageLoader.h +80 -0
  12. package/native/appkit/AppKitImageLoader.mm +156 -0
  13. package/native/appkit/AppKitKeyEvents.h +26 -0
  14. package/native/appkit/AppKitKeyEvents.mm +97 -0
  15. package/native/appkit/AppKitMenuBar.mm +424 -0
  16. package/native/appkit/AppKitMountingManager.h +135 -0
  17. package/native/appkit/AppKitMountingManager.mm +946 -0
  18. package/native/appkit/AppKitRunLoopObserver.h +34 -0
  19. package/native/appkit/AppKitRunLoopObserver.mm +42 -0
  20. package/native/appkit/AppKitScrollView.h +168 -0
  21. package/native/appkit/AppKitScrollView.mm +550 -0
  22. package/native/appkit/AppKitSkiaContext.h +33 -0
  23. package/native/appkit/AppKitSkiaContext.mm +255 -0
  24. package/native/appkit/AppKitSkiaModule.h +53 -0
  25. package/native/appkit/AppKitSkiaModule.mm +81 -0
  26. package/native/appkit/AppKitSkiaPeer.h +37 -0
  27. package/native/appkit/AppKitSkiaPeer.mm +123 -0
  28. package/native/appkit/AppKitSkiaView.h +62 -0
  29. package/native/appkit/AppKitSnapshot.h +21 -0
  30. package/native/appkit/AppKitSnapshot.mm +91 -0
  31. package/native/appkit/AppKitTextInput.h +140 -0
  32. package/native/appkit/AppKitTextInput.mm +588 -0
  33. package/native/appkit/AppKitTextPeer.h +74 -0
  34. package/native/appkit/AppKitTextPeer.mm +353 -0
  35. package/native/appkit/AppKitTitleBar.h +90 -0
  36. package/native/appkit/AppKitTitleBar.mm +186 -0
  37. package/native/appkit/AppKitTouchDispatcher.h +138 -0
  38. package/native/appkit/AppKitTouchDispatcher.mm +514 -0
  39. package/native/appkit/AppKitWindowControl.mm +222 -0
  40. package/native/appkit/AppKitWindowModule.h +62 -0
  41. package/native/appkit/AppKitWindowModule.mm +194 -0
  42. package/native/appkit/ColorSchemeAppKit.mm +71 -0
  43. package/native/appkit/ComponentRegistryAppKit.mm +121 -0
  44. package/native/appkit/CoreTextLayout.h +43 -0
  45. package/native/appkit/CoreTextLayout.mm +252 -0
  46. package/native/appkit/CoreTextLayoutManager.mm +138 -0
  47. package/native/appkit/FontRegistryCoreText.mm +148 -0
  48. package/native/appkit/PlatformServicesAppKit.mm +540 -0
  49. package/native/appkit/RnAppKitView.h +417 -0
  50. package/native/appkit/RnAppKitView.mm +1605 -0
  51. package/native/appkit/RnTextLayout.h +38 -0
  52. package/native/appkit/RnTextLayout.mm +168 -0
  53. package/native/appkit/demo_layout_appkit.mm +93 -0
  54. package/native/appkit/main_appkit.mm +1860 -0
  55. package/native/appkit/mount_harness_appkit.mm +197 -0
  56. package/native/tests/test_appkit_accessibility.mm +170 -0
  57. package/native/tests/test_appkit_image.mm +237 -0
  58. package/native/tests/test_appkit_input.mm +852 -0
  59. package/native/tests/test_appkit_key_events.mm +150 -0
  60. package/native/tests/test_appkit_mounting.mm +430 -0
  61. package/native/tests/test_appkit_scrollbar.mm +132 -0
  62. package/native/tests/test_appkit_services.mm +57 -0
  63. package/native/tests/test_appkit_skia.mm +212 -0
  64. package/native/tests/test_appkit_text.mm +180 -0
  65. package/native/tests/test_appkit_textinput.mm +748 -0
  66. package/native/tests/test_appkit_titlebar.mm +107 -0
  67. package/native/tests/test_appkit_view.mm +237 -0
  68. package/package.json +26 -0
  69. package/react-native.config.js +22 -0
@@ -0,0 +1,138 @@
1
+ // AppKit input into React Native's touch events.
2
+ //
3
+ // React Native's Pressability -- what backs <Pressable>, <TouchableOpacity> and
4
+ // every onPress in an app -- runs on the responder system in JavaScript, and the
5
+ // responder system is fed by touchstart/touchmove/touchend. So a pointer on a
6
+ // desktop is reported here as a single touch point, which is also what React
7
+ // Native for Windows and macOS do. W3C pointer events exist alongside these and
8
+ // are only consulted for hover, behind a feature flag.
9
+ //
10
+ // Deliberately the same shape as basalt-gtk's GtkTouchDispatcher,
11
+ // down to the state machine and the event construction, because the part that
12
+ // is easy to get subtly different -- which touches are in `touches` versus
13
+ // `changedTouches`, and which view a gesture is reported against -- is not
14
+ // about the toolkit at all. If the two ever diverge, an app's onPress will fire
15
+ // on one desktop and not the other.
16
+
17
+ #pragma once
18
+
19
+ #import "RnAppKitView.h"
20
+
21
+ #include "AppKitMountingManager.h"
22
+ #include "HoverTracker.h"
23
+ #include "PointerButtons.h"
24
+
25
+ #include <react/renderer/components/view/TouchEventEmitter.h>
26
+ #include <react/renderer/graphics/Point.h>
27
+
28
+ namespace basalt {
29
+
30
+ // The tag of the React Native view under a point, in the root's coordinates, or
31
+ // 0 if the point hits nothing. The view-tree half of this is
32
+ // `RnAppKitHitTest`, which lives in the view layer and needs no React Native.
33
+ facebook::react::Tag hitTestTag(RnAppKitView *root, double x, double y);
34
+
35
+ class AppKitTouchDispatcher {
36
+ public:
37
+ AppKitTouchDispatcher(AppKitMountingManager *mountingManager, RnAppKitView *surfaceRoot);
38
+ ~AppKitTouchDispatcher();
39
+
40
+ AppKitTouchDispatcher(const AppKitTouchDispatcher &) = delete;
41
+ AppKitTouchDispatcher &operator=(const AppKitTouchDispatcher &) = delete;
42
+ AppKitTouchDispatcher(AppKitTouchDispatcher &&) = delete;
43
+ AppKitTouchDispatcher &operator=(AppKitTouchDispatcher &&) = delete;
44
+
45
+ // Synthesises a press and release at a point in surface-root coordinates,
46
+ // entering at the same place the AppKit callbacks do.
47
+ //
48
+ // This exists because the input path is otherwise untestable in automation:
49
+ // synthesising a real mouse event on macOS means CGEvent, which needs
50
+ // accessibility permission an automated run does not have. It skips AppKit's
51
+ // event delivery and nothing else, so it proves hit testing, emitter lookup
52
+ // and event-beat delivery, but not that AppKit routes clicks here.
53
+ void synthesiseTap(double x, double y);
54
+ // The same, with a button. BASALT_TEST_SECONDARY_TAP is what needs it: a
55
+ // right-click cannot be injected any other way, and it is the one click whose
56
+ // whole point is that it does *not* press what it lands on.
57
+ void synthesiseTap(double x, double y, basalt::PointerButton button);
58
+
59
+ // A press, a run of moves, and a release. The same reason as synthesiseTap,
60
+ // one step further: a gesture recogniser cannot be exercised by a tap at all
61
+ // -- a pan is defined by the movement between the two -- so a drag has to be
62
+ // injectable for anything about it to be provable outside a person's hand.
63
+ void synthesiseDrag(double fromX, double fromY, double toX, double toY, int steps);
64
+
65
+ // Moves the pointer without pressing it, which is what produces hover. Same
66
+ // reason as the two above, and the same entry point the tracking area uses. A
67
+ // negative coordinate means the pointer left the surface.
68
+ void synthesiseHover(double x, double y);
69
+
70
+ // The main-thread half, called by the view that received the mouse event.
71
+ // Public because the Objective-C trampoline has to reach them.
72
+ // `button` decides whether this presses anything. Only the primary one drives
73
+ // the touch model; a secondary or middle click produces a pointer event and
74
+ // nothing else. See core/PointerButtons.h.
75
+ void dispatchTouchStart(double x, double y, basalt::PointerButton button);
76
+ void dispatchTouchMove(double x, double y);
77
+ void dispatchTouchEnd(double x, double y, basalt::PointerButton button);
78
+ void dispatchTouchCancel();
79
+
80
+ // Starts a drag out of the window if the press began on a view marked as a
81
+ // drag source. True when it did, and the caller should then cancel the
82
+ // touch: AppKit owns the gesture from that point and the touch will never
83
+ // end. See core/DragAndDrop.h.
84
+ bool beginDragOut(double x, double y);
85
+
86
+ // The hover half. Separate from dispatchTouchMove because the two answer
87
+ // different questions about the same motion: the touch model asks which view
88
+ // the finger went down on, and hover asks which views the cursor is inside
89
+ // right now.
90
+ void dispatchHover(double x, double y);
91
+ void dispatchHoverLeave();
92
+
93
+ private:
94
+ enum class TouchKind { Start, Move, End, Cancel };
95
+
96
+ // Hands the pointer to a gesture recogniser that has activated, cancelling
97
+ // React Native's touch. True when that happened, which means the caller has
98
+ // nothing left to report.
99
+ bool yieldToGesture(double x, double y);
100
+
101
+ // Builds the TouchEvent for the current gesture. React Native wants three
102
+ // lists -- all touches, the ones that changed, and the ones that started on
103
+ // the event target -- which with one pointer are the same list, except on
104
+ // touchend where nothing is touching any more.
105
+ void emit(TouchKind kind, facebook::react::Tag target, double x, double y);
106
+
107
+ // A pointerMove at the view under the cursor. The only pointer event a host
108
+ // emits for hover; see core/HoverTracker.h.
109
+ // A press or a release as a pointer event, which is the only form a
110
+ // non-primary click arrives in.
111
+ void emitPointerButton(bool down,
112
+ facebook::react::Tag target,
113
+ double originX,
114
+ double originY,
115
+ double x,
116
+ double y,
117
+ basalt::PointerButton button);
118
+ void emitPointerMove(
119
+ facebook::react::Tag target, double originX, double originY, double x, double y);
120
+
121
+ AppKitMountingManager *mountingManager_;
122
+ RnAppKitView *surfaceRoot_;
123
+ // The Objective-C object the root forwards mouse events to. Held so it
124
+ // outlives the root's weak reference to it.
125
+ id inputTarget_;
126
+
127
+ // The view a gesture started on. React Native reports every touch in a
128
+ // gesture against the target it began on, even after the pointer leaves that
129
+ // view, because that is what the responder system expects.
130
+ facebook::react::Tag activeTarget_{0};
131
+ bool isDown_{false};
132
+
133
+ // Where the cursor was, so that enter and leave can be told from over and
134
+ // out. See core/HoverTracker.h.
135
+ HoverTracker hover_;
136
+ };
137
+
138
+ } // namespace basalt
@@ -0,0 +1,514 @@
1
+ #import "AppKitTouchDispatcher.h"
2
+
3
+ #import "AppKitDropTarget.h"
4
+
5
+ #include "Gestures.h"
6
+
7
+ #include <react/renderer/components/view/PointerEvent.h>
8
+ #include <react/renderer/components/view/TouchEvent.h>
9
+
10
+ #include <cstdint>
11
+
12
+ // The bridge between AppKit's protocol and the C++ dispatcher. A C++ object
13
+ // cannot conform to an Objective-C protocol, and the root holds its handler
14
+ // weakly, so this exists and the dispatcher owns it.
15
+ @interface RnAppKitInputTarget : NSObject <RnAppKitInputHandler> {
16
+ // Whether this press has already been offered as a drag. See
17
+ // rnMouseDraggedTo:.
18
+ BOOL _dragAsked;
19
+ }
20
+ @property(nonatomic, assign) basalt::AppKitTouchDispatcher *dispatcher;
21
+ @end
22
+
23
+ @implementation RnAppKitInputTarget
24
+
25
+ - (void)rnMouseDownAt:(NSPoint)point button:(int)button {
26
+ // A new press: the drag question is open again.
27
+ _dragAsked = NO;
28
+ if (_dispatcher != nullptr) {
29
+ _dispatcher->dispatchTouchStart(point.x, point.y, static_cast<basalt::PointerButton>(button));
30
+ }
31
+ }
32
+
33
+ - (void)rnMouseDraggedTo:(NSPoint)point {
34
+ if (_dispatcher == nullptr) {
35
+ return;
36
+ }
37
+ // A drag out of the window, if the press started on a marked view. Asked
38
+ // here rather than on mouseDown: a press is not a drag until it moves, and
39
+ // asking on the way down would turn every press on a draggable view into
40
+ // one.
41
+ //
42
+ // Answered once per press: `beginDraggingSessionWithItems:` takes the
43
+ // gesture, and AppKit runs its own loop until the drop -- so the touch that
44
+ // started it never ends, and it is cancelled rather than left hanging.
45
+ if (!_dragAsked) {
46
+ _dragAsked = YES;
47
+ if (_dispatcher->beginDragOut(point.x, point.y)) {
48
+ _dispatcher->dispatchTouchCancel();
49
+ return;
50
+ }
51
+ }
52
+ _dispatcher->dispatchTouchMove(point.x, point.y);
53
+ }
54
+
55
+ - (void)rnMouseUpAt:(NSPoint)point button:(int)button {
56
+ if (_dispatcher != nullptr) {
57
+ _dispatcher->dispatchTouchEnd(point.x, point.y, static_cast<basalt::PointerButton>(button));
58
+ }
59
+ }
60
+
61
+ - (void)rnMouseMovedTo:(NSPoint)point {
62
+ if (_dispatcher != nullptr) {
63
+ _dispatcher->dispatchHover(point.x, point.y);
64
+ }
65
+ }
66
+
67
+ - (void)rnMouseExited {
68
+ if (_dispatcher != nullptr) {
69
+ _dispatcher->dispatchHoverLeave();
70
+ }
71
+ }
72
+
73
+ @end
74
+
75
+ namespace basalt {
76
+
77
+ using facebook::react::HighResTimeStamp;
78
+ using facebook::react::Point;
79
+ using facebook::react::PointerEvent;
80
+ using facebook::react::Tag;
81
+ using facebook::react::Touch;
82
+ using facebook::react::TouchEvent;
83
+ using facebook::react::TouchEventEmitter;
84
+ using facebook::react::Touches;
85
+
86
+ namespace {
87
+
88
+ // A desktop pointer is one touch point, and React Native identifies touches by
89
+ // number. Zero is the first (and here only) finger. The same number identifies
90
+ // the pointer in pointer events, where React Native keys its hover tracking by
91
+ // it -- and where zero is what React Native's own iOS host uses for a mouse.
92
+ constexpr int kPointerIdentifier = 0;
93
+
94
+ // The payload for a hovering pointer, at a point in root coordinates with the
95
+ // target's origin in the same space.
96
+ PointerEvent hoverEvent(double x, double y, double originX, double originY) {
97
+ PointerEvent event{};
98
+ event.pointerId = kPointerIdentifier;
99
+ event.pointerType = "mouse";
100
+ event.clientPoint = Point{.x = static_cast<facebook::react::Float>(x),
101
+ .y = static_cast<facebook::react::Float>(y)};
102
+ event.screenPoint = event.clientPoint;
103
+ event.offsetPoint = Point{.x = static_cast<facebook::react::Float>(x - originX),
104
+ .y = static_cast<facebook::react::Float>(y - originY)};
105
+ event.width = 1;
106
+ event.height = 1;
107
+ // A hovering mouse presses nothing. `button` is -1 rather than 0 because 0 is
108
+ // the left button; -1 is W3C's "no button changed state".
109
+ event.button = -1;
110
+ event.buttons = 0;
111
+ event.isPrimary = true;
112
+ event.timeStamp = HighResTimeStamp::now();
113
+ return event;
114
+ }
115
+
116
+ } // namespace
117
+
118
+ AppKitTouchDispatcher::AppKitTouchDispatcher(AppKitMountingManager *mountingManager,
119
+ RnAppKitView *surfaceRoot)
120
+ : mountingManager_(mountingManager), surfaceRoot_(surfaceRoot) {
121
+ RnAppKitInputTarget *target = [[RnAppKitInputTarget alloc] init];
122
+ target.dispatcher = this;
123
+ inputTarget_ = target;
124
+ surfaceRoot_.rnInputHandler = target;
125
+ // The root's tracking area is only wanted once there is somewhere to send
126
+ // hover, and AppKit will not ask again just because a property changed.
127
+ [surfaceRoot_ updateTrackingAreas];
128
+ }
129
+
130
+ AppKitTouchDispatcher::~AppKitTouchDispatcher() {
131
+ // The root's reference is weak, so it goes nil on its own; clearing the back
132
+ // pointer first means an event already in flight cannot reach a dead object.
133
+ ((RnAppKitInputTarget *)inputTarget_).dispatcher = nullptr;
134
+ inputTarget_ = nil;
135
+ }
136
+
137
+ void AppKitTouchDispatcher::synthesiseTap(double x, double y) {
138
+ synthesiseTap(x, y, basalt::PointerButton::Primary);
139
+ }
140
+
141
+ void AppKitTouchDispatcher::synthesiseTap(double x, double y, basalt::PointerButton button) {
142
+ dispatchTouchStart(x, y, button);
143
+ dispatchTouchEnd(x, y, button);
144
+ }
145
+
146
+ void AppKitTouchDispatcher::synthesiseHover(double x, double y) {
147
+ if (x < 0 || y < 0) {
148
+ dispatchHoverLeave();
149
+ return;
150
+ }
151
+ dispatchHover(x, y);
152
+ }
153
+
154
+ void AppKitTouchDispatcher::synthesiseDrag(double fromX, double fromY, double toX, double toY, int steps) {
155
+ dispatchTouchStart(fromX, fromY, basalt::PointerButton::Primary);
156
+ const int count = steps < 1 ? 1 : steps;
157
+ for (int step = 1; step <= count; step++) {
158
+ const double progress = static_cast<double>(step) / count;
159
+ dispatchTouchMove(fromX + (toX - fromX) * progress, fromY + (toY - fromY) * progress);
160
+ }
161
+ dispatchTouchEnd(toX, toY, basalt::PointerButton::Primary);
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Hit testing
166
+ // ---------------------------------------------------------------------------
167
+
168
+ bool AppKitTouchDispatcher::beginDragOut(double x, double y) {
169
+ return beginDragIfMarked(surfaceRoot_, x, y);
170
+ }
171
+
172
+ Tag hitTestTag(RnAppKitView *root, double x, double y) {
173
+ RnAppKitView *hit = RnAppKitHitTest(root, x, y);
174
+ return hit == nil ? 0 : static_cast<Tag>(hit.rnTag);
175
+ }
176
+
177
+ namespace {
178
+
179
+ // The React Native views under a point, innermost first, each with its origin
180
+ // in the root's coordinates. That is what a gesture recogniser needs and a
181
+ // touch does not: a touch is reported against one target, while a gesture may
182
+ // be attached to any ancestor of the view that was hit.
183
+ //
184
+ // Built only when something is attached; see core/Gestures.h.
185
+ std::vector<basalt::HitView> hitChain(RnAppKitView *root, double x, double y) {
186
+ std::vector<basalt::HitView> chain;
187
+ RnAppKitView *hit = RnAppKitHitTest(root, x, y);
188
+
189
+ for (NSView *view = hit; view != nil; view = view.superview) {
190
+ if (![view isKindOfClass:[RnAppKitView class]]) {
191
+ continue;
192
+ }
193
+ // The view's own origin in root coordinates. Conversion rather than summed
194
+ // frames, because a scrolled ancestor's bounds origin has to count and
195
+ // AppKit already knows how.
196
+ const NSPoint origin = [view convertPoint:NSZeroPoint toView:root];
197
+ chain.push_back(basalt::HitView{.tag = static_cast<int>(((RnAppKitView *)view).rnTag),
198
+ .originX = origin.x,
199
+ .originY = origin.y});
200
+ if (view == root) {
201
+ break;
202
+ }
203
+ }
204
+ return chain;
205
+ }
206
+
207
+ } // namespace
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Dispatch
211
+ // ---------------------------------------------------------------------------
212
+
213
+ // The innermost view under a point, and where inside it that point is. The
214
+ // pointer event wants both whichever button it was; PointerEventsProcessor
215
+ // walks up from there itself.
216
+ static Tag targetUnder(RnAppKitView *surfaceRoot, double x, double y, NSPoint *origin) {
217
+ RnAppKitView *hit = RnAppKitHitTest(surfaceRoot, x, y);
218
+ for (NSView *view = hit; view != nil; view = view.superview) {
219
+ if ([view isKindOfClass:[RnAppKitView class]]) {
220
+ *origin = [view convertPoint:NSZeroPoint toView:surfaceRoot];
221
+ return static_cast<Tag>(((RnAppKitView *)view).rnTag);
222
+ }
223
+ if (view == surfaceRoot) {
224
+ break;
225
+ }
226
+ }
227
+ return 0;
228
+ }
229
+
230
+ void AppKitTouchDispatcher::dispatchTouchStart(double x, double y, basalt::PointerButton button) {
231
+ NSPoint origin = NSZeroPoint;
232
+ const Tag target = targetUnder(surfaceRoot_, x, y, &origin);
233
+ if (target == 0) {
234
+ return;
235
+ }
236
+
237
+ // Every button produces a pointer event, which is where `button` can be said
238
+ // at all.
239
+ emitPointerButton(true, target, origin.x, origin.y, x, y, button);
240
+
241
+ // Only the primary one goes any further. A secondary click is not a press --
242
+ // the web fires no `click` for one and no desktop treats it as an activation
243
+ // -- so it must not reach the responder system, a gesture handler, or
244
+ // `onPress`. See core/PointerButtons.h.
245
+ if (!basalt::isPressButton(button)) {
246
+ return;
247
+ }
248
+
249
+ if (!basalt::gestures().empty()) {
250
+ basalt::gestures().pointerDown(
251
+ hitChain(surfaceRoot_, x, y), x, y, basalt::monotonicMilliseconds());
252
+ }
253
+
254
+ activeTarget_ = target;
255
+ isDown_ = true;
256
+ emit(TouchKind::Start, target, x, y);
257
+
258
+ // A gesture that claimed the pointer on contact -- a native handler -- takes
259
+ // it away from React Native's responder system immediately.
260
+ yieldToGesture(x, y);
261
+ }
262
+
263
+ void AppKitTouchDispatcher::dispatchTouchMove(double x, double y) {
264
+ if (!basalt::gestures().empty()) {
265
+ basalt::gestures().pointerMove(x, y, basalt::monotonicMilliseconds());
266
+ }
267
+
268
+ // Motion with no button down is hover, which the touch model has no place
269
+ // for. Reporting it would look to the responder system like a finger dragging
270
+ // across the screen at all times.
271
+ if (!isDown_ || activeTarget_ == 0) {
272
+ return;
273
+ }
274
+ if (yieldToGesture(x, y)) {
275
+ return;
276
+ }
277
+ emit(TouchKind::Move, activeTarget_, x, y);
278
+ }
279
+
280
+ void AppKitTouchDispatcher::dispatchTouchEnd(double x, double y, basalt::PointerButton button) {
281
+ // The release half of the pointer event, for every button. Reported against
282
+ // whatever is under the pointer now rather than what was under it on press,
283
+ // which is what a release means when nothing was captured.
284
+ {
285
+ NSPoint origin = NSZeroPoint;
286
+ const Tag over = targetUnder(surfaceRoot_, x, y, &origin);
287
+ if (over != 0) {
288
+ emitPointerButton(false, over, origin.x, origin.y, x, y, button);
289
+ }
290
+ }
291
+
292
+ // And nothing else for a button that never pressed anything -- there is no
293
+ // touch to end, no gesture to finish and no <Switch> to toggle.
294
+ if (!basalt::isPressButton(button)) {
295
+ return;
296
+ }
297
+
298
+ if (!basalt::gestures().empty()) {
299
+ basalt::gestures().pointerUp(x, y, basalt::monotonicMilliseconds());
300
+ }
301
+
302
+ if (!isDown_ || activeTarget_ == 0) {
303
+ return;
304
+ }
305
+ const Tag target = activeTarget_;
306
+ isDown_ = false;
307
+ activeTarget_ = 0;
308
+ emit(TouchKind::End, target, x, y);
309
+
310
+ // A <Switch> is toggled from here rather than by the toolkit control, so
311
+ // that a press means the same thing on three desktops -- the Windows one is
312
+ // painted and has no widget to click at all. A no-op for every view that is
313
+ // not a switch.
314
+ if (mountingManager_ != nullptr) {
315
+ mountingManager_->pressedView(target);
316
+ }
317
+ }
318
+
319
+ void AppKitTouchDispatcher::dispatchTouchCancel() {
320
+ if (!basalt::gestures().empty()) {
321
+ basalt::gestures().pointerCancel();
322
+ }
323
+
324
+ if (!isDown_ || activeTarget_ == 0) {
325
+ return;
326
+ }
327
+ const Tag target = activeTarget_;
328
+ isDown_ = false;
329
+ activeTarget_ = 0;
330
+ emit(TouchKind::Cancel, target, 0, 0);
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Hover
335
+ // ---------------------------------------------------------------------------
336
+
337
+ void AppKitTouchDispatcher::dispatchHover(double x, double y) {
338
+ // Only the innermost view is reported against -- React Native's
339
+ // PointerEventsProcessor walks up from it itself -- but the whole chain is
340
+ // still needed, because a listener on any ancestor is reason to dispatch.
341
+ Tag target = 0;
342
+ NSPoint origin = NSZeroPoint;
343
+ std::uint16_t listeners = HoverListenerNone;
344
+ RnAppKitView *hit = RnAppKitHitTest(surfaceRoot_, x, y);
345
+ for (NSView *view = hit; view != nil; view = view.superview) {
346
+ if ([view isKindOfClass:[RnAppKitView class]]) {
347
+ const auto tag = static_cast<Tag>(((RnAppKitView *)view).rnTag);
348
+ if (target == 0) {
349
+ target = tag;
350
+ origin = [view convertPoint:NSZeroPoint toView:surfaceRoot_];
351
+ }
352
+ listeners |= mountingManager_->hoverListenersForTag(tag);
353
+ }
354
+ if (view == surfaceRoot_) {
355
+ break;
356
+ }
357
+ }
358
+
359
+ if (target == 0) {
360
+ dispatchHoverLeave();
361
+ return;
362
+ }
363
+ if (!hover_.admitMove(static_cast<int>(target), listeners)) {
364
+ return;
365
+ }
366
+ emitPointerMove(target, origin.x, origin.y, x, y);
367
+ }
368
+
369
+ void AppKitTouchDispatcher::dispatchHoverLeave() {
370
+ const int target = hover_.admitLeave();
371
+ if (target == 0) {
372
+ return;
373
+ }
374
+ const auto emitter = std::dynamic_pointer_cast<const TouchEventEmitter>(
375
+ mountingManager_->eventEmitterForTag(static_cast<Tag>(target)));
376
+ if (emitter == nullptr) {
377
+ return;
378
+ }
379
+ // A leave from a platform is not forwarded: the processor reads it as the
380
+ // pointer being gone and unwinds the path it is holding. So the coordinates
381
+ // on it are never seen by an app, and zero is the honest answer for a
382
+ // position that no longer exists.
383
+ emitter->onPointerLeave(hoverEvent(0, 0, 0, 0));
384
+ }
385
+
386
+ void AppKitTouchDispatcher::emitPointerButton(bool down,
387
+ Tag target,
388
+ double originX,
389
+ double originY,
390
+ double x,
391
+ double y,
392
+ basalt::PointerButton button) {
393
+ const auto emitter = std::dynamic_pointer_cast<const TouchEventEmitter>(
394
+ mountingManager_->eventEmitterForTag(target));
395
+ if (emitter == nullptr) {
396
+ return;
397
+ }
398
+ PointerEvent event = hoverEvent(x, y, originX, originY);
399
+ event.button = static_cast<int>(button);
400
+ // Held *during* the event, which is a different number from `button` in the
401
+ // same event: on release nothing is held any more. See core/PointerButtons.h.
402
+ event.buttons = down ? static_cast<int>(basalt::buttonsMaskFor(button)) : 0;
403
+ if (down) {
404
+ emitter->onPointerDown(event);
405
+ } else {
406
+ emitter->onPointerUp(event);
407
+ }
408
+ }
409
+
410
+ void AppKitTouchDispatcher::emitPointerMove(
411
+ Tag target, double originX, double originY, double x, double y) {
412
+ const auto emitter = std::dynamic_pointer_cast<const TouchEventEmitter>(
413
+ mountingManager_->eventEmitterForTag(target));
414
+ if (emitter == nullptr) {
415
+ return;
416
+ }
417
+ emitter->onPointerMove(hoverEvent(x, y, originX, originY));
418
+ }
419
+
420
+ // A gesture recogniser that has activated owns the pointer, and React Native's
421
+ // responder system must be told the touch it was following is gone -- otherwise
422
+ // panning across a <Pressable> pans *and* presses it. This is what RNGH's
423
+ // `setJSResponder` does on the platforms it was written for; here both sides
424
+ // are fed from this one place, so it is a cancel rather than a negotiation.
425
+ bool AppKitTouchDispatcher::yieldToGesture(double x, double y) {
426
+ if (!isDown_ || activeTarget_ == 0 || !basalt::gestures().hasActiveHandler()) {
427
+ return false;
428
+ }
429
+ const Tag target = activeTarget_;
430
+ isDown_ = false;
431
+ activeTarget_ = 0;
432
+ emit(TouchKind::Cancel, target, x, y);
433
+ return true;
434
+ }
435
+
436
+ // Where a page point falls inside one view, for `Touch::offsetPoint`.
437
+ //
438
+ // The walk is `rnPageToLocal:fromRoot:into:`, on the view, so that it can be
439
+ // tested without a dispatcher. The page point is the fallback, which is what
440
+ // every touch carried before: the target unmounted between the press and this
441
+ // event, or a chain with no inverse. Refusing would drop the touch.
442
+ static NSPoint pointInView(basalt::AppKitMountingManager *manager,
443
+ RnAppKitView *root,
444
+ facebook::react::Tag target,
445
+ double pageX,
446
+ double pageY) {
447
+ const NSPoint page = NSMakePoint(pageX, pageY);
448
+ RnAppKitView *view = manager == nullptr ? nil : manager->viewForTag(target);
449
+ if (view == nil || root == nil) {
450
+ return page;
451
+ }
452
+
453
+ NSPoint local = page;
454
+ return [view rnPageToLocal:page fromRoot:root into:&local] ? local : page;
455
+ }
456
+
457
+ void AppKitTouchDispatcher::emit(TouchKind kind, Tag target, double x, double y) {
458
+ const auto emitter = std::dynamic_pointer_cast<const TouchEventEmitter>(
459
+ mountingManager_->eventEmitterForTag(target));
460
+ if (emitter == nullptr) {
461
+ return;
462
+ }
463
+
464
+ const auto now = HighResTimeStamp::now();
465
+
466
+ Touch touch{};
467
+ touch.identifier = kPointerIdentifier;
468
+ touch.target = target;
469
+ // Coordinates arrive relative to the surface root, which is what React Native
470
+ // calls the page.
471
+ touch.pagePoint = Point{.x = static_cast<facebook::react::Float>(x),
472
+ .y = static_cast<facebook::react::Float>(y)};
473
+ touch.screenPoint = touch.pagePoint;
474
+
475
+ // Relative to the target, which is what `offsetPoint` means and what
476
+ // `locationX`/`locationY` are built from. It carried the page point until
477
+ // now: right only for a view at the surface's origin, and wrong by that
478
+ // view's position for every other.
479
+ const NSPoint offset = pointInView(mountingManager_, surfaceRoot_, target, x, y);
480
+ touch.offsetPoint = Point{.x = static_cast<facebook::react::Float>(offset.x),
481
+ .y = static_cast<facebook::react::Float>(offset.y)};
482
+ touch.force = 1.0F;
483
+ touch.timeStamp = now;
484
+
485
+ Touches changed{};
486
+ changed.insert(touch);
487
+
488
+ const bool isEnd = kind == TouchKind::End || kind == TouchKind::Cancel;
489
+
490
+ TouchEvent event{};
491
+ event.changedTouches = changed;
492
+ // On touchend the finger is gone, so it is no longer in `touches`. Getting
493
+ // this wrong leaves the responder system believing a touch is still active,
494
+ // which swallows the next press.
495
+ event.touches = isEnd ? Touches{} : changed;
496
+ event.targetTouches = event.touches;
497
+
498
+ switch (kind) {
499
+ case TouchKind::Start:
500
+ emitter->onTouchStart(event);
501
+ break;
502
+ case TouchKind::Move:
503
+ emitter->onTouchMove(event);
504
+ break;
505
+ case TouchKind::End:
506
+ emitter->onTouchEnd(event);
507
+ break;
508
+ case TouchKind::Cancel:
509
+ emitter->onTouchCancel(event);
510
+ break;
511
+ }
512
+ }
513
+
514
+ } // namespace basalt