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.
- package/LICENSE +21 -0
- package/cli/runMacos.js +33 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +298 -0
- package/native/appkit/AppKitAnimationChoreographer.h +54 -0
- package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
- package/native/appkit/AppKitDropTarget.h +34 -0
- package/native/appkit/AppKitDropTarget.mm +292 -0
- package/native/appkit/AppKitFocus.h +120 -0
- package/native/appkit/AppKitFocus.mm +293 -0
- package/native/appkit/AppKitImageLoader.h +80 -0
- package/native/appkit/AppKitImageLoader.mm +156 -0
- package/native/appkit/AppKitKeyEvents.h +26 -0
- package/native/appkit/AppKitKeyEvents.mm +97 -0
- package/native/appkit/AppKitMenuBar.mm +424 -0
- package/native/appkit/AppKitMountingManager.h +135 -0
- package/native/appkit/AppKitMountingManager.mm +946 -0
- package/native/appkit/AppKitRunLoopObserver.h +34 -0
- package/native/appkit/AppKitRunLoopObserver.mm +42 -0
- package/native/appkit/AppKitScrollView.h +168 -0
- package/native/appkit/AppKitScrollView.mm +550 -0
- package/native/appkit/AppKitSkiaContext.h +33 -0
- package/native/appkit/AppKitSkiaContext.mm +255 -0
- package/native/appkit/AppKitSkiaModule.h +53 -0
- package/native/appkit/AppKitSkiaModule.mm +81 -0
- package/native/appkit/AppKitSkiaPeer.h +37 -0
- package/native/appkit/AppKitSkiaPeer.mm +123 -0
- package/native/appkit/AppKitSkiaView.h +62 -0
- package/native/appkit/AppKitSnapshot.h +21 -0
- package/native/appkit/AppKitSnapshot.mm +91 -0
- package/native/appkit/AppKitTextInput.h +140 -0
- package/native/appkit/AppKitTextInput.mm +588 -0
- package/native/appkit/AppKitTextPeer.h +74 -0
- package/native/appkit/AppKitTextPeer.mm +353 -0
- package/native/appkit/AppKitTitleBar.h +90 -0
- package/native/appkit/AppKitTitleBar.mm +186 -0
- package/native/appkit/AppKitTouchDispatcher.h +138 -0
- package/native/appkit/AppKitTouchDispatcher.mm +514 -0
- package/native/appkit/AppKitWindowControl.mm +222 -0
- package/native/appkit/AppKitWindowModule.h +62 -0
- package/native/appkit/AppKitWindowModule.mm +194 -0
- package/native/appkit/ColorSchemeAppKit.mm +71 -0
- package/native/appkit/ComponentRegistryAppKit.mm +121 -0
- package/native/appkit/CoreTextLayout.h +43 -0
- package/native/appkit/CoreTextLayout.mm +252 -0
- package/native/appkit/CoreTextLayoutManager.mm +138 -0
- package/native/appkit/FontRegistryCoreText.mm +148 -0
- package/native/appkit/PlatformServicesAppKit.mm +540 -0
- package/native/appkit/RnAppKitView.h +417 -0
- package/native/appkit/RnAppKitView.mm +1605 -0
- package/native/appkit/RnTextLayout.h +38 -0
- package/native/appkit/RnTextLayout.mm +168 -0
- package/native/appkit/demo_layout_appkit.mm +93 -0
- package/native/appkit/main_appkit.mm +1860 -0
- package/native/appkit/mount_harness_appkit.mm +197 -0
- package/native/tests/test_appkit_accessibility.mm +170 -0
- package/native/tests/test_appkit_image.mm +237 -0
- package/native/tests/test_appkit_input.mm +852 -0
- package/native/tests/test_appkit_key_events.mm +150 -0
- package/native/tests/test_appkit_mounting.mm +430 -0
- package/native/tests/test_appkit_scrollbar.mm +132 -0
- package/native/tests/test_appkit_services.mm +57 -0
- package/native/tests/test_appkit_skia.mm +212 -0
- package/native/tests/test_appkit_text.mm +180 -0
- package/native/tests/test_appkit_textinput.mm +748 -0
- package/native/tests/test_appkit_titlebar.mm +107 -0
- package/native/tests/test_appkit_view.mm +237 -0
- package/package.json +26 -0
- 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
|