@symbiote-native/engine 0.1.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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/build/accessibility-info/index.android.d.ts +3 -0
  4. package/build/accessibility-info/index.android.js +166 -0
  5. package/build/accessibility-info/index.d.ts +1 -0
  6. package/build/accessibility-info/index.ios.d.ts +3 -0
  7. package/build/accessibility-info/index.ios.js +219 -0
  8. package/build/accessibility-info/index.js +5 -0
  9. package/build/accessibility-info/shared.d.ts +34 -0
  10. package/build/accessibility-info/shared.js +13 -0
  11. package/build/action-sheet-ios/index.d.ts +36 -0
  12. package/build/action-sheet-ios/index.js +74 -0
  13. package/build/alert/index.android.d.ts +5 -0
  14. package/build/alert/index.android.js +117 -0
  15. package/build/alert/index.d.ts +1 -0
  16. package/build/alert/index.ios.d.ts +7 -0
  17. package/build/alert/index.ios.js +83 -0
  18. package/build/alert/index.js +8 -0
  19. package/build/alert/shared.d.ts +19 -0
  20. package/build/alert/shared.js +17 -0
  21. package/build/animated/animated-component-shared.d.ts +5 -0
  22. package/build/animated/animated-component-shared.js +54 -0
  23. package/build/animated/animation.d.ts +9 -0
  24. package/build/animated/animation.js +6 -0
  25. package/build/animated/animations/base.d.ts +27 -0
  26. package/build/animated/animations/base.js +90 -0
  27. package/build/animated/animations/composition.d.ts +38 -0
  28. package/build/animated/animations/composition.js +236 -0
  29. package/build/animated/animations/decay.d.ts +22 -0
  30. package/build/animated/animations/decay.js +65 -0
  31. package/build/animated/animations/raf.d.ts +5 -0
  32. package/build/animated/animations/raf.js +39 -0
  33. package/build/animated/animations/spring-config.d.ts +6 -0
  34. package/build/animated/animations/spring-config.js +55 -0
  35. package/build/animated/animations/spring.d.ts +50 -0
  36. package/build/animated/animations/spring.js +207 -0
  37. package/build/animated/animations/timing.d.ts +27 -0
  38. package/build/animated/animations/timing.js +101 -0
  39. package/build/animated/animations/tracking.d.ts +14 -0
  40. package/build/animated/animations/tracking.js +43 -0
  41. package/build/animated/bezier.d.ts +1 -0
  42. package/build/animated/bezier.js +101 -0
  43. package/build/animated/color.d.ts +37 -0
  44. package/build/animated/color.js +183 -0
  45. package/build/animated/easing.d.ts +20 -0
  46. package/build/animated/easing.js +96 -0
  47. package/build/animated/event.d.ts +36 -0
  48. package/build/animated/event.js +252 -0
  49. package/build/animated/graph.d.ts +38 -0
  50. package/build/animated/graph.js +227 -0
  51. package/build/animated/index.d.ts +20 -0
  52. package/build/animated/index.js +28 -0
  53. package/build/animated/interpolation-node.d.ts +16 -0
  54. package/build/animated/interpolation-node.js +57 -0
  55. package/build/animated/interpolation.d.ts +22 -0
  56. package/build/animated/interpolation.js +199 -0
  57. package/build/animated/mock.d.ts +56 -0
  58. package/build/animated/mock.js +127 -0
  59. package/build/animated/native/native-animated.d.ts +43 -0
  60. package/build/animated/native/native-animated.js +146 -0
  61. package/build/animated/operators.d.ts +80 -0
  62. package/build/animated/operators.js +266 -0
  63. package/build/animated/props.d.ts +20 -0
  64. package/build/animated/props.js +187 -0
  65. package/build/animated/style.d.ts +26 -0
  66. package/build/animated/style.js +187 -0
  67. package/build/animated/value-xy.d.ts +35 -0
  68. package/build/animated/value-xy.js +106 -0
  69. package/build/animated/value.d.ts +36 -0
  70. package/build/animated/value.js +185 -0
  71. package/build/app-registry/index.d.ts +40 -0
  72. package/build/app-registry/index.js +144 -0
  73. package/build/app-state/index.d.ts +16 -0
  74. package/build/app-state/index.js +105 -0
  75. package/build/appearance/index.d.ts +12 -0
  76. package/build/appearance/index.js +84 -0
  77. package/build/back-handler/index.d.ts +14 -0
  78. package/build/back-handler/index.js +106 -0
  79. package/build/commit.d.ts +16 -0
  80. package/build/commit.js +678 -0
  81. package/build/debug.d.ts +5 -0
  82. package/build/debug.js +18 -0
  83. package/build/dimensions/index.d.ts +28 -0
  84. package/build/dimensions/index.js +148 -0
  85. package/build/dispatch.d.ts +2 -0
  86. package/build/dispatch.js +18 -0
  87. package/build/events/index.d.ts +1 -0
  88. package/build/events/index.js +691 -0
  89. package/build/fabric.d.ts +32 -0
  90. package/build/fabric.js +59 -0
  91. package/build/host-instance/index.d.ts +11 -0
  92. package/build/host-instance/index.js +49 -0
  93. package/build/i18n-manager/index.d.ts +13 -0
  94. package/build/i18n-manager/index.js +91 -0
  95. package/build/index.d.ts +80 -0
  96. package/build/index.js +72 -0
  97. package/build/interaction-manager/index.d.ts +45 -0
  98. package/build/interaction-manager/index.js +222 -0
  99. package/build/keyboard/index.d.ts +31 -0
  100. package/build/keyboard/index.js +142 -0
  101. package/build/layout-animation/index.d.ts +66 -0
  102. package/build/layout-animation/index.js +183 -0
  103. package/build/linking/index.android.d.ts +2 -0
  104. package/build/linking/index.android.js +18 -0
  105. package/build/linking/index.d.ts +1 -0
  106. package/build/linking/index.ios.d.ts +2 -0
  107. package/build/linking/index.ios.js +9 -0
  108. package/build/linking/index.js +6 -0
  109. package/build/linking/shared.d.ts +32 -0
  110. package/build/linking/shared.js +98 -0
  111. package/build/native-events.d.ts +24 -0
  112. package/build/native-events.js +129 -0
  113. package/build/native-modules.d.ts +6 -0
  114. package/build/native-modules.js +57 -0
  115. package/build/node.d.ts +36 -0
  116. package/build/node.js +194 -0
  117. package/build/pan-responder/index.d.ts +53 -0
  118. package/build/pan-responder/index.js +353 -0
  119. package/build/permissions-android/index.d.ts +115 -0
  120. package/build/permissions-android/index.js +185 -0
  121. package/build/pixel-ratio/index.d.ts +8 -0
  122. package/build/pixel-ratio/index.js +27 -0
  123. package/build/platform/index.android.d.ts +22 -0
  124. package/build/platform/index.android.js +60 -0
  125. package/build/platform/index.d.ts +1 -0
  126. package/build/platform/index.ios.d.ts +18 -0
  127. package/build/platform/index.ios.js +62 -0
  128. package/build/platform/index.js +5 -0
  129. package/build/platform/shared.d.ts +25 -0
  130. package/build/platform/shared.js +41 -0
  131. package/build/platform-color.d.ts +19 -0
  132. package/build/platform-color.js +25 -0
  133. package/build/post-commit.d.ts +4 -0
  134. package/build/post-commit.js +16 -0
  135. package/build/process-aspect-ratio.d.ts +1 -0
  136. package/build/process-aspect-ratio.js +34 -0
  137. package/build/process-background-image/index.d.ts +28 -0
  138. package/build/process-background-image/index.js +557 -0
  139. package/build/process-box-shadow/index.d.ts +11 -0
  140. package/build/process-box-shadow/index.js +193 -0
  141. package/build/process-filter.d.ts +31 -0
  142. package/build/process-filter.js +304 -0
  143. package/build/process-font-variant.d.ts +1 -0
  144. package/build/process-font-variant.js +17 -0
  145. package/build/process-transform/index.d.ts +5 -0
  146. package/build/process-transform/index.js +120 -0
  147. package/build/process-transform-origin/index.d.ts +3 -0
  148. package/build/process-transform-origin/index.js +108 -0
  149. package/build/registry.d.ts +31 -0
  150. package/build/registry.js +145 -0
  151. package/build/settings/index.d.ts +8 -0
  152. package/build/settings/index.js +126 -0
  153. package/build/share/index.android.d.ts +3 -0
  154. package/build/share/index.android.js +56 -0
  155. package/build/share/index.d.ts +1 -0
  156. package/build/share/index.ios.d.ts +3 -0
  157. package/build/share/index.ios.js +47 -0
  158. package/build/share/index.js +6 -0
  159. package/build/share/shared.d.ts +32 -0
  160. package/build/share/shared.js +32 -0
  161. package/build/status-bar/index.android.d.ts +5 -0
  162. package/build/status-bar/index.android.js +83 -0
  163. package/build/status-bar/index.d.ts +1 -0
  164. package/build/status-bar/index.ios.d.ts +5 -0
  165. package/build/status-bar/index.ios.js +66 -0
  166. package/build/status-bar/index.js +4 -0
  167. package/build/status-bar/shared.d.ts +22 -0
  168. package/build/status-bar/shared.js +22 -0
  169. package/build/style/index.d.ts +1 -0
  170. package/build/style/index.js +30 -0
  171. package/build/style-registry/index.d.ts +11 -0
  172. package/build/style-registry/index.js +165 -0
  173. package/build/style-sheet/index.d.ts +20 -0
  174. package/build/style-sheet/index.js +121 -0
  175. package/build/styles.d.ts +220 -0
  176. package/build/styles.js +7 -0
  177. package/build/surface.d.ts +16 -0
  178. package/build/surface.js +67 -0
  179. package/build/tags.d.ts +1 -0
  180. package/build/tags.js +10 -0
  181. package/build/text-input-state.d.ts +5 -0
  182. package/build/text-input-state.js +29 -0
  183. package/build/toast-android/index.d.ts +10 -0
  184. package/build/toast-android/index.js +108 -0
  185. package/build/vibration/index.android.d.ts +2 -0
  186. package/build/vibration/index.android.js +18 -0
  187. package/build/vibration/index.d.ts +1 -0
  188. package/build/vibration/index.ios.d.ts +2 -0
  189. package/build/vibration/index.ios.js +54 -0
  190. package/build/vibration/index.js +6 -0
  191. package/build/vibration/shared.d.ts +15 -0
  192. package/build/vibration/shared.js +68 -0
  193. package/build/view-config.d.ts +1 -0
  194. package/build/view-config.js +114 -0
  195. package/package.json +41 -0
@@ -0,0 +1,691 @@
1
+ // Event normalization. Fabric delivers raw touch primitives to a single global
2
+ // handler, with the instanceHandle (our SymbioteNode) as the target. There is no
3
+ // raw `press` event; a tap is synthesized from a touch sequence: the start and
4
+ // end targets are correlated so a press fires only when the touch ends on the
5
+ // node it started on (or a descendant). Bubbling events walk target -> root,
6
+ // invoking each ancestor's listener until one calls stopPropagation. Layout is a
7
+ // direct event in RN and is delivered only to its own target.
8
+ import { dlog } from '../debug';
9
+ import { runWrapped } from '../dispatch';
10
+ import { getSlot } from '../fabric';
11
+ import { isAnchor, isSymbioteNode } from '../node';
12
+ import { registeredNativeEvent } from '../registry';
13
+ // Raw Fabric event name -> listener name. Generic bubbling events live here; press
14
+ // is synthesized from a touch sequence and layout is direct, so both are handled
15
+ // outside this table.
16
+ // Raw Fabric event -> listener name, split by dispatch phase. Press is synthesized
17
+ // from a touch sequence (handled separately below); everything else is table-driven.
18
+ // Bubbling events walk target -> root; direct events fire only on the target.
19
+ const BUBBLING_EVENTS = {
20
+ topFocus: 'focus',
21
+ topBlur: 'blur',
22
+ topChange: 'change',
23
+ topEndEditing: 'endEditing',
24
+ topSubmitEditing: 'submitEditing',
25
+ topKeyPress: 'keyPress',
26
+ };
27
+ const DIRECT_EVENTS = {
28
+ topLayout: 'layout',
29
+ topScroll: 'scroll',
30
+ topScrollBeginDrag: 'scrollBeginDrag',
31
+ topScrollEndDrag: 'scrollEndDrag',
32
+ topMomentumScrollBegin: 'momentumScrollBegin',
33
+ topMomentumScrollEnd: 'momentumScrollEnd',
34
+ topSelectionChange: 'selectionChange',
35
+ topContentSizeChange: 'contentSizeChange',
36
+ topLoadStart: 'loadStart',
37
+ topLoad: 'load',
38
+ topLoadEnd: 'loadEnd',
39
+ topError: 'error',
40
+ topProgress: 'progress',
41
+ topPartialLoad: 'partialLoad',
42
+ topRefresh: 'refresh',
43
+ topShow: 'show',
44
+ topRequestClose: 'requestClose',
45
+ topDismiss: 'dismiss',
46
+ topOrientationChange: 'orientationChange',
47
+ // Text glyph layout (onTextLayout) and the iOS status-bar-tap scroll-to-top.
48
+ topTextLayout: 'textLayout',
49
+ topScrollToTop: 'scrollToTop',
50
+ // Accessibility events from RN's base ViewConfig; any view can emit them.
51
+ // accessibilityAction fires on iOS + Android; the iOS-only three (accessibilityTap,
52
+ // magicTap, accessibilityEscape) have no Android producer, so they are inert there.
53
+ topAccessibilityAction: 'accessibilityAction',
54
+ topAccessibilityTap: 'accessibilityTap',
55
+ topMagicTap: 'magicTap',
56
+ topAccessibilityEscape: 'accessibilityEscape',
57
+ };
58
+ const TOUCH_START = 'topTouchStart';
59
+ const TOUCH_MOVE = 'topTouchMove';
60
+ const TOUCH_END = 'topTouchEnd';
61
+ const TOUCH_CANCEL = 'topTouchCancel';
62
+ const PRESS = 'press';
63
+ // Responder protocol (PanResponder / Touchable). RN's two-phase negotiation:
64
+ // every should-set is asked CAPTURE (root -> target) then BUBBLE (target -> root),
65
+ // and the first node returning true wins, on a touch START *and* on every MOVE,
66
+ // so a node can claim the responder mid-gesture. If someone already holds it, the
67
+ // incumbent is asked onResponderTerminationRequest; a true answer (or no listener)
68
+ // hands it over (terminate + grant), a false answer rejects the taker. Lifecycle
69
+ // events are direct (grant/start/move/end/release/terminate/reject). Listener names
70
+ // are post-`on` (onResponderMove -> 'responderMove').
71
+ const START_SHOULD_SET = 'startShouldSetResponder';
72
+ const START_SHOULD_SET_CAPTURE = 'startShouldSetResponderCapture';
73
+ const MOVE_SHOULD_SET = 'moveShouldSetResponder';
74
+ const MOVE_SHOULD_SET_CAPTURE = 'moveShouldSetResponderCapture';
75
+ const RESPONDER_GRANT = 'responderGrant';
76
+ const RESPONDER_REJECT = 'responderReject';
77
+ const RESPONDER_START = 'responderStart';
78
+ const RESPONDER_MOVE = 'responderMove';
79
+ const RESPONDER_END = 'responderEnd';
80
+ const RESPONDER_RELEASE = 'responderRelease';
81
+ const RESPONDER_TERMINATE = 'responderTerminate';
82
+ const RESPONDER_TERMINATION_REQUEST = 'responderTerminationRequest';
83
+ // Synthesized alongside press so Pressable can show pressed-state feedback: both
84
+ // fire on the node the touch STARTED on (the responder), pressOut on end/cancel.
85
+ const PRESS_IN = 'pressIn';
86
+ const PRESS_OUT = 'pressOut';
87
+ // Synthesized from a sustained hold so bare Text/View onLongPress fires without a
88
+ // native event (Pressable runs the same timer in JS). Default delay matches RN's
89
+ // Touchable (500ms); a fired long press suppresses the tap on release.
90
+ const LONG_PRESS = 'longPress';
91
+ const DEFAULT_LONG_PRESS_MS = 500;
92
+ // Pressability cancels the pending long press when the touch drifts past this many
93
+ // points from where it started (Pressability.DEFAULT_LONG_PRESS_DEACTIVATION_DISTANCE).
94
+ const LONG_PRESS_DEACTIVATION_DISTANCE = 10;
95
+ // RN's bank is indexed by touch identifier and warns above 20; we never warn (headless
96
+ // events may carry larger or absent ids), we just skip anything out of a sane range.
97
+ const MAX_TOUCH_BANK = 20;
98
+ const touchBank = [];
99
+ const touchHistory = {
100
+ touchBank,
101
+ numberActiveTouches: 0,
102
+ indexOfSingleActiveTouch: -1,
103
+ mostRecentTimeStamp: 0,
104
+ };
105
+ function toFiniteNumber(value) {
106
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
107
+ }
108
+ // Pull a recordable touch out of an untyped entry, or undefined when it lacks a usable
109
+ // identifier or coordinates. RN's getTouchIdentifier throws on a null id; we skip
110
+ // instead, so events without touch geometry leave the bank untouched.
111
+ function normalizeTouch(raw) {
112
+ if (!isRecord(raw))
113
+ return undefined;
114
+ const identifier = toFiniteNumber(raw.identifier);
115
+ const pageX = toFiniteNumber(raw.pageX);
116
+ const pageY = toFiniteNumber(raw.pageY);
117
+ if (identifier === undefined || pageX === undefined || pageY === undefined)
118
+ return undefined;
119
+ if (identifier < 0 || identifier > MAX_TOUCH_BANK)
120
+ return undefined;
121
+ return { identifier, pageX, pageY, timestamp: toFiniteNumber(raw.timestamp) ?? 0 };
122
+ }
123
+ // The changed touches for this frame (start/move/end), defensively read.
124
+ function changedTouchesOf(nativeEvent) {
125
+ const raw = nativeEvent.changedTouches;
126
+ if (!Array.isArray(raw))
127
+ return [];
128
+ const out = [];
129
+ for (const entry of raw) {
130
+ const touch = normalizeTouch(entry);
131
+ if (touch !== undefined)
132
+ out.push(touch);
133
+ }
134
+ return out;
135
+ }
136
+ // Count of all touches still down (RN reads nativeEvent.touches.length directly).
137
+ function activeTouchCount(nativeEvent) {
138
+ const raw = nativeEvent.touches;
139
+ return Array.isArray(raw) ? raw.length : 0;
140
+ }
141
+ function recordTouchStart(touch) {
142
+ const record = touchBank[touch.identifier];
143
+ if (record) {
144
+ record.touchActive = true;
145
+ record.startPageX = touch.pageX;
146
+ record.startPageY = touch.pageY;
147
+ record.startTimeStamp = touch.timestamp;
148
+ record.currentPageX = touch.pageX;
149
+ record.currentPageY = touch.pageY;
150
+ record.currentTimeStamp = touch.timestamp;
151
+ record.previousPageX = touch.pageX;
152
+ record.previousPageY = touch.pageY;
153
+ record.previousTimeStamp = touch.timestamp;
154
+ }
155
+ else {
156
+ touchBank[touch.identifier] = {
157
+ touchActive: true,
158
+ startPageX: touch.pageX,
159
+ startPageY: touch.pageY,
160
+ startTimeStamp: touch.timestamp,
161
+ currentPageX: touch.pageX,
162
+ currentPageY: touch.pageY,
163
+ currentTimeStamp: touch.timestamp,
164
+ previousPageX: touch.pageX,
165
+ previousPageY: touch.pageY,
166
+ previousTimeStamp: touch.timestamp,
167
+ };
168
+ }
169
+ touchHistory.mostRecentTimeStamp = touch.timestamp;
170
+ }
171
+ // Move and end share the previous<-current shift; only `touchActive` differs.
172
+ function shiftTouchRecord(touch, active) {
173
+ const record = touchBank[touch.identifier];
174
+ if (!record)
175
+ return;
176
+ record.touchActive = active;
177
+ record.previousPageX = record.currentPageX;
178
+ record.previousPageY = record.currentPageY;
179
+ record.previousTimeStamp = record.currentTimeStamp;
180
+ record.currentPageX = touch.pageX;
181
+ record.currentPageY = touch.pageY;
182
+ record.currentTimeStamp = touch.timestamp;
183
+ touchHistory.mostRecentTimeStamp = touch.timestamp;
184
+ }
185
+ // Maintain the bank as a touch frame flows. Mirrors RN's recordTouchTrack: moveish
186
+ // shifts records, startish records + recomputes numberActiveTouches, endish marks the
187
+ // record inactive + rescans for the single remaining touch. `kind` is the touch phase.
188
+ function recordTouchTrack(kind, nativeEvent) {
189
+ if (kind === 'move') {
190
+ for (const touch of changedTouchesOf(nativeEvent))
191
+ shiftTouchRecord(touch, true);
192
+ return;
193
+ }
194
+ if (kind === 'start') {
195
+ for (const touch of changedTouchesOf(nativeEvent))
196
+ recordTouchStart(touch);
197
+ touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
198
+ if (touchHistory.numberActiveTouches === 1) {
199
+ const first = normalizeTouch(arrayFirst(nativeEvent.touches));
200
+ touchHistory.indexOfSingleActiveTouch = first?.identifier ?? -1;
201
+ }
202
+ return;
203
+ }
204
+ for (const touch of changedTouchesOf(nativeEvent))
205
+ shiftTouchRecord(touch, false);
206
+ touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
207
+ if (touchHistory.numberActiveTouches === 1) {
208
+ for (let i = 0; i < touchBank.length; i++) {
209
+ const record = touchBank[i];
210
+ if (record !== undefined && record.touchActive) {
211
+ touchHistory.indexOfSingleActiveTouch = i;
212
+ break;
213
+ }
214
+ }
215
+ }
216
+ }
217
+ function arrayFirst(value) {
218
+ return Array.isArray(value) ? value[0] : undefined;
219
+ }
220
+ // Drop all touch state. Called on a fully-released / cancelled gesture so a stale bank
221
+ // never leaks geometry into the next gesture's first frame.
222
+ function resetTouchHistory() {
223
+ touchBank.length = 0;
224
+ touchHistory.numberActiveTouches = 0;
225
+ touchHistory.indexOfSingleActiveTouch = -1;
226
+ touchHistory.mostRecentTimeStamp = 0;
227
+ }
228
+ // Attach the live touch history onto the event the responder handlers receive, matching
229
+ // ResponderEventPlugin.js (`grantEvent.touchHistory = ...`, etc.). PanResponder reads
230
+ // it for the per-touch dx/vx math; handlers that ignore it are unaffected.
231
+ function attachTouchHistory(nativeEvent) {
232
+ nativeEvent.touchHistory = touchHistory;
233
+ }
234
+ // #endregion
235
+ let installed = false;
236
+ // Target of the in-flight touch, remembered at topTouchStart and consumed (or
237
+ // cleared) at topTouchEnd / topTouchCancel.
238
+ let pressStart;
239
+ // The node that claimed the responder for the in-flight touch (PanResponder), or
240
+ // undefined when nobody claimed it. Receives move and release/terminate.
241
+ let currentResponder;
242
+ // Long-press synthesis: armed at touch start when some node in the press path listens
243
+ // for it, fired once after the hold delay, disarmed on end/cancel; the same arm/clear
244
+ // lifecycle Pressable runs in JS. Pressability ALSO cancels the timer when the touch
245
+ // drifts past LONG_PRESS_DEACTIVATION_DISTANCE, so we record the start point at touch
246
+ // start and clear the timer on a move that exceeds it.
247
+ let longPressTimer;
248
+ let longPressFired = false;
249
+ // Touch coordinate at touch start (pageX/pageY), or undefined when the native event
250
+ // carried no coords; then the move-distance cancel is simply skipped.
251
+ let longPressStart;
252
+ function clearLongPress() {
253
+ if (longPressTimer !== undefined) {
254
+ clearTimeout(longPressTimer);
255
+ longPressTimer = undefined;
256
+ }
257
+ }
258
+ // Read the gesture's page coordinate from a raw native touch event, defensively: RN
259
+ // puts pageX/pageY on the event for a single touch, or on the first entry of a
260
+ // `touches` array for multi-touch. Returns undefined when neither shape carries
261
+ // numbers, so callers skip any coordinate-dependent logic rather than guess.
262
+ function readTouchPoint(nativeEvent) {
263
+ const fromPair = (source) => {
264
+ if (!source)
265
+ return undefined;
266
+ const { pageX, pageY } = source;
267
+ if (typeof pageX === 'number' && typeof pageY === 'number')
268
+ return { x: pageX, y: pageY };
269
+ return undefined;
270
+ };
271
+ const direct = fromPair(nativeEvent);
272
+ if (direct)
273
+ return direct;
274
+ const touches = nativeEvent.touches;
275
+ if (Array.isArray(touches)) {
276
+ const first = touches[0];
277
+ if (isRecord(first))
278
+ return fromPair(first);
279
+ }
280
+ return undefined;
281
+ }
282
+ // Narrow an unknown to a plain object so its properties can be read without a cast.
283
+ function isRecord(value) {
284
+ return typeof value === 'object' && value !== null;
285
+ }
286
+ // Whether any touch still down started inside the responder (its target IS the
287
+ // responder or a descendant). RN's noResponderTouches walks nativeEvent.touches and
288
+ // returns false the moment one is found; a release fires only when none remain. The
289
+ // headless smokes fire with an empty `{}` event (no `touches`) → no remaining touch →
290
+ // release fires, preserving single-touch behavior. (ResponderEventPlugin.noResponder-
291
+ // Touches + isAncestor.)
292
+ function hasRemainingResponderTouch(responder, nativeEvent) {
293
+ const touches = nativeEvent.touches;
294
+ if (!Array.isArray(touches))
295
+ return false;
296
+ for (const touch of touches) {
297
+ if (!isRecord(touch))
298
+ continue;
299
+ const target = touch.target;
300
+ if (isSymbioteNode(target) && endsWithin(target, responder))
301
+ return true;
302
+ }
303
+ return false;
304
+ }
305
+ // Whether any node from `target` up to the root listens for `listenerName`, used to
306
+ // arm the long-press timer only when a handler would actually receive it.
307
+ function hasListenerInPath(target, listenerName) {
308
+ for (let node = target; node; node = node.parent) {
309
+ if (node.listeners?.has(listenerName) === true)
310
+ return true;
311
+ }
312
+ return false;
313
+ }
314
+ // Invoke one node's own listener (no bubbling) and hand back its return value, so
315
+ // the responder negotiation can read the boolean from onStartShouldSetResponder.
316
+ function callOwnListener(node, listenerName, nativeEvent) {
317
+ const listener = node.listeners?.get(listenerName);
318
+ if (!listener)
319
+ return undefined;
320
+ return listener({
321
+ type: listenerName,
322
+ target: node,
323
+ currentTarget: node,
324
+ nativeEvent,
325
+ stopPropagation: () => { },
326
+ });
327
+ }
328
+ // The node chain from `from` up to the root, deepest first. The single allocation
329
+ // the two-phase walk indexes both ways (capture reads it reversed).
330
+ function pathToRoot(from) {
331
+ const path = [];
332
+ for (let node = from; node; node = node.parent)
333
+ path.push(node);
334
+ return path;
335
+ }
336
+ // Depth of a node below the root (root = 0). Aligns two nodes before the lockstep
337
+ // climb to their lowest common ancestor.
338
+ function depthOf(node) {
339
+ let depth = 0;
340
+ for (let n = node.parent; n; n = n.parent)
341
+ depth++;
342
+ return depth;
343
+ }
344
+ // RN's getLowestCommonAncestor over our parent pointers: lift the deeper node to the
345
+ // shallower one's depth, then climb both in lockstep until they meet (ResponderEvent-
346
+ // Plugin.getLowestCommonAncestor). Used to scope the move re-negotiation.
347
+ function lowestCommonAncestor(a, b) {
348
+ let da = depthOf(a);
349
+ let db = depthOf(b);
350
+ let na = a;
351
+ let nb = b;
352
+ while (na && da > db) {
353
+ na = na.parent;
354
+ da--;
355
+ }
356
+ while (nb && db > da) {
357
+ nb = nb.parent;
358
+ db--;
359
+ }
360
+ while (na && nb) {
361
+ if (na === nb)
362
+ return na;
363
+ na = na.parent;
364
+ nb = nb.parent;
365
+ }
366
+ return undefined;
367
+ }
368
+ // RN's two-phase should-set walk: CAPTURE root -> deepest, then BUBBLE deepest -> root;
369
+ // the first node returning true wins. `skip` is excluded from both passes; RN skips
370
+ // the deepest node when it IS the current responder (you don't ask the holder to
371
+ // re-claim), so its should-set callback never consumes the gesture frame out from under
372
+ // its own onResponderMove (PanResponder folds geometry in the should-set-capture
373
+ // handler, so asking the responder again would zero its move).
374
+ function findWantsResponder(path, captureName, bubbleName, nativeEvent, skip) {
375
+ for (let i = path.length - 1; i >= 0; i--) {
376
+ if (path[i] !== skip && callOwnListener(path[i], captureName, nativeEvent) === true) {
377
+ return path[i];
378
+ }
379
+ }
380
+ for (const node of path) {
381
+ if (node !== skip && callOwnListener(node, bubbleName, nativeEvent) === true)
382
+ return node;
383
+ }
384
+ return undefined;
385
+ }
386
+ // Negotiate (or re-negotiate) the responder for a touch start/move. If nobody holds
387
+ // it, the winner is granted. If someone does, the incumbent is asked to relinquish
388
+ // via onResponderTerminationRequest (absent listener = implicit yes); on yes it is
389
+ // terminated and the taker granted, on no the taker is rejected.
390
+ function negotiateResponder(target, phase, nativeEvent) {
391
+ // With no responder, ask the full path from the touch target. With one, RN scopes
392
+ // the walk to the lowest common ancestor of responder+target upward (never below
393
+ // the responder) and skips the deepest node when it IS the responder (Responder-
394
+ // EventPlugin.setResponderAndExtractTransfer). At touch start currentResponder is
395
+ // cleared, so this collapses to the plain target->root start walk.
396
+ const from = currentResponder === undefined ? target : lowestCommonAncestor(currentResponder, target);
397
+ if (!from)
398
+ return;
399
+ const path = pathToRoot(from);
400
+ const skip = from === currentResponder ? from : undefined;
401
+ const wants = phase === 'start'
402
+ ? findWantsResponder(path, START_SHOULD_SET_CAPTURE, START_SHOULD_SET, nativeEvent, skip)
403
+ : findWantsResponder(path, MOVE_SHOULD_SET_CAPTURE, MOVE_SHOULD_SET, nativeEvent, skip);
404
+ if (!wants || wants === currentResponder)
405
+ return;
406
+ if (currentResponder === undefined) {
407
+ currentResponder = wants;
408
+ dlog(`responder granted to ${wants.component}`);
409
+ callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
410
+ return;
411
+ }
412
+ const incumbent = currentResponder;
413
+ // A missing termination-request listener means implicit consent (RN default true);
414
+ // only an explicit non-true answer keeps the incumbent and rejects the taker.
415
+ const guarded = incumbent.listeners?.has(RESPONDER_TERMINATION_REQUEST) === true;
416
+ const allowed = !guarded || callOwnListener(incumbent, RESPONDER_TERMINATION_REQUEST, nativeEvent) === true;
417
+ if (allowed) {
418
+ // RN's transfer order (setResponderAndExtractTransfer): grant the TAKER first, then
419
+ // terminate the incumbent. RN dispatches grant ahead of the terminationRequest too,
420
+ // purely to read the taker's block-native return; we have no native surface to
421
+ // block, so on the REJECT path firing a grant the taker never keeps would be a
422
+ // visible no-op event with no behavioral counterpart. We therefore fire grant
423
+ // before terminate on the consent path (matching RN's grant<terminate ordering) and
424
+ // omit it on reject; the consent OUTCOME is unchanged either way.
425
+ dlog(`responder transferred ${incumbent.component} -> ${wants.component}`);
426
+ callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
427
+ callOwnListener(incumbent, RESPONDER_TERMINATE, nativeEvent);
428
+ currentResponder = wants;
429
+ }
430
+ else {
431
+ dlog(`responder takeover of ${incumbent.component} rejected`);
432
+ callOwnListener(wants, RESPONDER_REJECT, nativeEvent);
433
+ }
434
+ }
435
+ export function installEventHandler() {
436
+ if (installed)
437
+ return;
438
+ installed = true;
439
+ getSlot().registerEventHandler((instanceHandle, topLevelType, nativeEvent) => {
440
+ if (!isSymbioteNode(instanceHandle))
441
+ return;
442
+ if (topLevelType === TOUCH_START) {
443
+ dlog(`event ${TOUCH_START}`);
444
+ // Update the touch bank, then attach it so responder handlers (PanResponder)
445
+ // read each touch's own previous->current delta; RN records before dispatch.
446
+ recordTouchTrack('start', nativeEvent);
447
+ attachTouchHistory(nativeEvent);
448
+ pressStart = instanceHandle;
449
+ // Arm long-press synthesis: only when a listener exists in the path, fired once
450
+ // after the hold delay, then suppresses the tap (longPressFired) on release.
451
+ longPressFired = false;
452
+ clearLongPress();
453
+ longPressStart = readTouchPoint(nativeEvent);
454
+ if (hasListenerInPath(instanceHandle, LONG_PRESS)) {
455
+ const longPressTarget = instanceHandle;
456
+ longPressTimer = setTimeout(() => {
457
+ longPressTimer = undefined;
458
+ longPressFired = true;
459
+ dlog('synthesized longPress -> dispatch');
460
+ runWrapped(() => bubble(longPressTarget, LONG_PRESS, nativeEvent));
461
+ }, DEFAULT_LONG_PRESS_MS);
462
+ }
463
+ runWrapped(() => {
464
+ bubble(instanceHandle, PRESS_IN, nativeEvent);
465
+ // Responder negotiation runs alongside press synthesis: a View can be both
466
+ // a Pressable (press) and a PanResponder target (responder).
467
+ negotiateResponder(instanceHandle, 'start', nativeEvent);
468
+ // onResponderStart is a direct event to whoever now holds the responder.
469
+ if (currentResponder)
470
+ callOwnListener(currentResponder, RESPONDER_START, nativeEvent);
471
+ });
472
+ return;
473
+ }
474
+ if (topLevelType === TOUCH_MOVE) {
475
+ recordTouchTrack('move', nativeEvent);
476
+ attachTouchHistory(nativeEvent);
477
+ // Cancel the pending long press if the touch drifted too far (Pressability's
478
+ // deactivation-distance check). Skipped when either coord is unknown.
479
+ if (longPressTimer !== undefined && longPressStart) {
480
+ const here = readTouchPoint(nativeEvent);
481
+ if (here) {
482
+ const dx = here.x - longPressStart.x;
483
+ const dy = here.y - longPressStart.y;
484
+ if (Math.hypot(dx, dy) > LONG_PRESS_DEACTIVATION_DISTANCE) {
485
+ dlog('longPress cancelled (moved past deactivation distance)');
486
+ clearLongPress();
487
+ }
488
+ }
489
+ }
490
+ runWrapped(() => {
491
+ // Re-negotiate first: a node can claim the responder mid-gesture via
492
+ // onMoveShouldSetResponder (the responder itself is skipped, see negotiate).
493
+ negotiateResponder(instanceHandle, 'move', nativeEvent);
494
+ // The only consumer of a move is the responder; without one, RN drops it too.
495
+ if (currentResponder)
496
+ callOwnListener(currentResponder, RESPONDER_MOVE, nativeEvent);
497
+ });
498
+ return;
499
+ }
500
+ if (topLevelType === TOUCH_END) {
501
+ recordTouchTrack('end', nativeEvent);
502
+ attachTouchHistory(nativeEvent);
503
+ const start = pressStart;
504
+ pressStart = undefined;
505
+ const responder = currentResponder;
506
+ // RN releases (and clears) the responder only when no remaining touch still down
507
+ // started inside it; lifting ONE finger in a multi-touch gesture must NOT release.
508
+ // onResponderEnd still fires on every finger-up. (ResponderEventPlugin: responderEnd
509
+ // is unconditional, responderRelease is gated on noResponderTouches.)
510
+ const releases = responder !== undefined && !hasRemainingResponderTouch(responder, nativeEvent);
511
+ if (releases)
512
+ currentResponder = undefined;
513
+ // A completed long press eats the tap (RN), but pressOut still fires below.
514
+ const wasLongPress = longPressFired;
515
+ longPressFired = false;
516
+ longPressStart = undefined;
517
+ clearLongPress();
518
+ runWrapped(() => {
519
+ if (start) {
520
+ // press fires only on an honest tap (ended within the responder); pressOut
521
+ // always fires on the responder so its pressed-state can release.
522
+ if (endsWithin(instanceHandle, start)) {
523
+ if (wasLongPress) {
524
+ dlog('press suppressed (longPress already fired)');
525
+ }
526
+ else {
527
+ dlog('event press -> dispatch');
528
+ bubble(start, PRESS, nativeEvent);
529
+ }
530
+ }
531
+ bubble(start, PRESS_OUT, nativeEvent);
532
+ }
533
+ else {
534
+ dlog(`event ${TOUCH_END} ignored (no matching start)`);
535
+ }
536
+ // onResponderEnd fires on every finger-up; onResponderRelease (the final
537
+ // release) only when the last responder touch lifted.
538
+ if (responder) {
539
+ callOwnListener(responder, RESPONDER_END, nativeEvent);
540
+ if (releases)
541
+ callOwnListener(responder, RESPONDER_RELEASE, nativeEvent);
542
+ else
543
+ dlog('responderEnd without release (touches remain inside responder)');
544
+ }
545
+ });
546
+ // Once no touch is down, clear the bank so the next gesture starts clean.
547
+ if (touchHistory.numberActiveTouches === 0)
548
+ resetTouchHistory();
549
+ return;
550
+ }
551
+ if (topLevelType === TOUCH_CANCEL) {
552
+ recordTouchTrack('end', nativeEvent);
553
+ attachTouchHistory(nativeEvent);
554
+ const start = pressStart;
555
+ pressStart = undefined;
556
+ const responder = currentResponder;
557
+ currentResponder = undefined;
558
+ longPressFired = false;
559
+ longPressStart = undefined;
560
+ clearLongPress();
561
+ runWrapped(() => {
562
+ if (start)
563
+ bubble(start, PRESS_OUT, nativeEvent);
564
+ // A cancelled gesture ends then terminates (the responder was taken away).
565
+ if (responder) {
566
+ callOwnListener(responder, RESPONDER_END, nativeEvent);
567
+ callOwnListener(responder, RESPONDER_TERMINATE, nativeEvent);
568
+ }
569
+ });
570
+ if (touchHistory.numberActiveTouches === 0)
571
+ resetTouchHistory();
572
+ return;
573
+ }
574
+ const direct = DIRECT_EVENTS[topLevelType];
575
+ if (direct !== undefined) {
576
+ dlog(`event ${topLevelType} -> ${direct} (direct)`);
577
+ runWrapped(() => deliverDirect(instanceHandle, direct, nativeEvent));
578
+ return;
579
+ }
580
+ const bubbling = BUBBLING_EVENTS[topLevelType];
581
+ if (bubbling !== undefined) {
582
+ dlog(`event ${topLevelType} -> ${bubbling} (bubble)`);
583
+ runWrapped(() => bubble(instanceHandle, bubbling, nativeEvent));
584
+ return;
585
+ }
586
+ // Third-party Fabric views (registerComponent) declare their own events; the
587
+ // built-in tables above don't know them, so fall back to the registry, keyed by
588
+ // the node's own component. `direct` events fire only on their target, the rest
589
+ // bubble: same split as the built-ins.
590
+ const registered = registeredNativeEvent(instanceHandle.component, topLevelType);
591
+ if (registered !== undefined) {
592
+ const phase = registered.direct ? 'direct' : 'bubble';
593
+ dlog(`event ${topLevelType} -> ${registered.listener} (${phase}, registered)`);
594
+ runWrapped(() => registered.direct
595
+ ? deliverDirect(instanceHandle, registered.listener, nativeEvent)
596
+ : bubble(instanceHandle, registered.listener, nativeEvent));
597
+ return;
598
+ }
599
+ // Nothing claimed this event: neither a built-in table nor the view's derived
600
+ // config. A permanent diagnostic seam: if a native view fires something we drop
601
+ // on the floor (an event the ViewConfig didn't surface, or a name mismatch),
602
+ // this is where it shows up. Keeps "the handler silently did nothing" debuggable.
603
+ dlog(`event ${topLevelType} UNMATCHED on ${instanceHandle.component} (dropped)`);
604
+ });
605
+ }
606
+ // A press is honest only if the touch ends on the node it started on, or a
607
+ // descendant of it: walk parent pointers up from the end target looking for the
608
+ // start target. The start node may have been unmounted mid-touch (parent pointer
609
+ // cleared); the walk simply runs out and returns false, no throw.
610
+ function endsWithin(endTarget, start) {
611
+ let node = endTarget;
612
+ while (node) {
613
+ if (node === start)
614
+ return true;
615
+ node = node.parent;
616
+ }
617
+ return false;
618
+ }
619
+ // Two-phase delivery, mirroring RN's accumulateTwoPhaseDispatches (legacy-events/
620
+ // EventPropagators): CAPTURE root -> target first, invoking each node's
621
+ // `<EventName>Capture` listener, then BUBBLE target -> root invoking the plain
622
+ // listener. The same event object semantics apply to both passes; a stopPropagation
623
+ // in capture halts before bubble ever runs. `target` stays the original node;
624
+ // `currentTarget` tracks whose listener runs.
625
+ function bubble(target, listenerName, nativeEvent) {
626
+ let stopped = false;
627
+ const stopPropagation = () => {
628
+ stopped = true;
629
+ };
630
+ // Capture phase: root -> target. RN gathers captured listeners first (the
631
+ // `<EventName>Capture` registration), so on*Capture handlers fire ahead of the
632
+ // bubble pass. The path is built target -> root, then walked in reverse to get
633
+ // root -> target without a second allocation.
634
+ const captureName = `${listenerName}Capture`;
635
+ const path = pathToRoot(target);
636
+ for (let i = path.length - 1; i >= 0; i--) {
637
+ const node = path[i];
638
+ // Anchors (Angular's #anchor component hosts) never paint and have no native view — a
639
+ // listener registered on one only exists because a framework's own output-binding
640
+ // machinery (already delivered directly, e.g. Angular's EventEmitter.subscribe) also
641
+ // registered it through Renderer2.listen. Bubbling into it would refire that same
642
+ // callback a second time, so anchors are transparent to listener lookup, not just paint.
643
+ const listener = isAnchor(node) ? undefined : node.listeners?.get(captureName);
644
+ if (listener) {
645
+ dlog(`event ${listenerName} capture on ${node.component}`);
646
+ listener({
647
+ type: listenerName,
648
+ target,
649
+ currentTarget: node,
650
+ nativeEvent,
651
+ stopPropagation,
652
+ });
653
+ if (stopped)
654
+ return;
655
+ }
656
+ }
657
+ // Bubble phase: target -> root, invoking each ancestor's plain listener. Anchors are
658
+ // transparent here too (see the capture-phase comment above) — same event, same reason.
659
+ let node = target;
660
+ while (node) {
661
+ const listener = isAnchor(node) ? undefined : node.listeners?.get(listenerName);
662
+ if (listener) {
663
+ // engine owner adds currentTarget + stopPropagation to SymbioteEvent
664
+ const event = {
665
+ type: listenerName,
666
+ target,
667
+ currentTarget: node,
668
+ nativeEvent,
669
+ stopPropagation,
670
+ };
671
+ listener(event);
672
+ if (stopped)
673
+ return;
674
+ }
675
+ node = node.parent;
676
+ }
677
+ }
678
+ // Direct (non-bubbling) delivery: only the target's own listener fires.
679
+ function deliverDirect(target, listenerName, nativeEvent) {
680
+ const listener = target.listeners?.get(listenerName);
681
+ if (!listener)
682
+ return;
683
+ // engine owner adds currentTarget + stopPropagation to SymbioteEvent
684
+ listener({
685
+ type: listenerName,
686
+ target,
687
+ currentTarget: target,
688
+ nativeEvent,
689
+ stopPropagation: () => { },
690
+ });
691
+ }