@symbiote-native/engine 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/android/CMakeLists.txt +51 -0
- package/android/build.gradle +90 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/SymbioteEngineJni.cpp +72 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEngineModule.kt +43 -0
- package/android/src/main/java/dev/symbiotenative/engine/SymbioteEnginePackage.kt +35 -0
- package/build/accessibility-info/shared.js +1 -1
- package/build/accessibility-props.d.ts +1 -7
- package/build/accessibility-props.js +22 -21
- package/build/animated/animations/composition.d.ts +1 -1
- package/build/animated/animations/composition.js +18 -4
- package/build/animated/easing.d.ts +3 -2
- package/build/animated/easing.js +17 -88
- package/build/animated/event.js +6 -1
- package/build/animated/graph.d.ts +2 -0
- package/build/animated/graph.js +14 -0
- package/build/animated/host-binding.d.ts +39 -0
- package/build/animated/host-binding.js +278 -0
- package/build/animated/index.d.ts +1 -1
- package/build/animated/leaf-lifecycle.js +10 -22
- package/build/animated/mock.d.ts +1 -19
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.js +16 -50
- package/build/events/index.js +123 -33
- package/build/fabric-props.d.ts +1 -1
- package/build/fabric-props.js +129 -182
- package/build/fabric.d.ts +10 -0
- package/build/fabric.js +40 -0
- package/build/host-access.d.ts +125 -0
- package/build/host-access.js +280 -0
- package/build/host-behavior.d.ts +107 -7
- package/build/host-behavior.js +309 -31
- package/build/image-source-write.d.ts +16 -0
- package/build/image-source-write.js +65 -0
- package/build/imperative.d.ts +49 -0
- package/build/imperative.js +258 -0
- package/build/index.d.ts +18 -10
- package/build/index.js +65 -10
- package/build/mutation-buffer.d.ts +222 -0
- package/build/mutation-buffer.js +491 -0
- package/build/native-engine.d.ts +182 -0
- package/build/native-engine.js +178 -0
- package/build/native-tree-host.d.ts +25 -0
- package/build/native-tree-host.js +66 -0
- package/build/node.d.ts +191 -60
- package/build/node.js +1034 -327
- package/build/pan-responder/index.d.ts +2 -2
- package/build/pan-responder/index.js +37 -56
- package/build/platform-color/index.d.ts +1 -1
- package/build/platform-color/index.js +11 -4
- package/build/process-background-image/index.js +30 -566
- package/build/process-background-longhands.d.ts +4 -0
- package/build/process-background-longhands.js +44 -0
- package/build/process-box-shadow/index.js +23 -187
- package/build/process-filter.js +27 -300
- package/build/process-transform/index.d.ts +1 -1
- package/build/process-transform/index.js +25 -107
- package/build/process-transform-origin/index.d.ts +1 -1
- package/build/process-transform-origin/index.js +29 -102
- package/build/registry.d.ts +36 -0
- package/build/registry.js +73 -0
- package/build/sound-manager/index.d.ts +3 -0
- package/build/sound-manager/index.js +36 -0
- package/build/structured-style.d.ts +10 -0
- package/build/structured-style.js +180 -0
- package/build/style-registry/index.d.ts +14 -0
- package/build/style-registry/index.js +60 -11
- package/build/styles.d.ts +5 -1
- package/build/surface.d.ts +31 -2
- package/build/surface.js +138 -42
- package/build/text-input-state.d.ts +1 -0
- package/build/text-input-state.js +17 -3
- package/build/tree-host.d.ts +307 -0
- package/build/tree-host.js +211 -0
- package/build/view-config.js +4 -4
- package/codegen-specs/NativeSymbioteEngine.ts +27 -0
- package/cpp/SymbioteDebug.cpp +51 -0
- package/cpp/SymbioteDebug.h +54 -0
- package/cpp/SymbioteEngineBindings.cpp +232 -0
- package/cpp/SymbioteEngineBindings.h +59 -0
- package/cpp/SymbioteFabricProps.cpp +2619 -0
- package/cpp/SymbioteFabricProps.h +223 -0
- package/cpp/SymbioteTree.cpp +2478 -0
- package/cpp/SymbioteTree.h +257 -0
- package/ios/SymbioteEngineModule.h +25 -0
- package/ios/SymbioteEngineModule.mm +44 -0
- package/package.json +31 -3
- package/react-native.config.cjs +23 -0
- package/symbiote-engine.podspec +42 -0
- package/build/animated/bezier.d.ts +0 -1
- package/build/animated/bezier.js +0 -102
- package/build/commit.d.ts +0 -49
- package/build/commit.js +0 -1030
- package/build/tags.d.ts +0 -2
- package/build/tags.js +0 -40
package/build/animated/rgba.js
CHANGED
|
@@ -8,45 +8,25 @@
|
|
|
8
8
|
// which surfaces at load time as `TypeError: Class extends value undefined is not a
|
|
9
9
|
// constructor` — AnimatedWithChildren is still uninitialized when color.ts evaluates. Keeping
|
|
10
10
|
// the pure half in its own leaf module breaks it structurally rather than by import ordering.
|
|
11
|
+
// No types are published for this package, and an ambient .d.ts is not picked up by another
|
|
12
|
+
// package's separate TS program (see symbiote-rn-import-testability) - so the suppression is
|
|
13
|
+
// local, at the import site.
|
|
14
|
+
// @ts-expect-error - untyped, @noflow plain JS that every toolchain here parses as-is.
|
|
15
|
+
import normalizeColorUpstream from '@react-native/normalize-colors';
|
|
11
16
|
export const DEFAULT_COLOR = { r: 0, g: 0, b: 0, a: 1 };
|
|
12
|
-
// Decompose
|
|
13
|
-
//
|
|
14
|
-
// the default rather than throwing inside a render. Exported so
|
|
15
|
-
// color path
|
|
17
|
+
// Decompose any color RN itself accepts into channels: #hex, rgb()/rgba(), hsl()/hsla(), hwb(),
|
|
18
|
+
// the 150 CSS names, `transparent`, and a 0xRRGGBBAA number. undefined when unparseable, so the
|
|
19
|
+
// caller falls back to the default rather than throwing inside a render. Exported so
|
|
20
|
+
// interpolation's color path decodes through this one decoder rather than duplicating it.
|
|
21
|
+
//
|
|
22
|
+
// The parsing is upstream's. A hand-rolled version lived here and knew hex and rgb() only, so
|
|
23
|
+
// `new AnimatedColor('red')` animated from BLACK - the fallback, not an error, and nothing was
|
|
24
|
+
// red anywhere. Its packed output is rrggbbaa, which is the order the shifts below assume.
|
|
16
25
|
export function normalizeColor(color) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
r: (c >>> 24) & 255,
|
|
21
|
-
g: (c >>> 16) & 255,
|
|
22
|
-
b: (c >>> 8) & 255,
|
|
23
|
-
a: (c & 255) / 255,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
const trimmed = color.trim();
|
|
27
|
-
if (trimmed.startsWith('#'))
|
|
28
|
-
return parseHex(trimmed);
|
|
29
|
-
if (/^rgba?\(/i.test(trimmed))
|
|
30
|
-
return parseRgb(trimmed);
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
function parseHex(hex) {
|
|
34
|
-
let body = hex.slice(1);
|
|
35
|
-
if (body.length === 3 || body.length === 4) {
|
|
36
|
-
body = body
|
|
37
|
-
.split('')
|
|
38
|
-
.map(c => c + c)
|
|
39
|
-
.join('');
|
|
40
|
-
}
|
|
41
|
-
if (body.length !== 6 && body.length !== 8)
|
|
26
|
+
const packed = normalizeColorUpstream(color);
|
|
27
|
+
if (typeof packed !== 'number')
|
|
42
28
|
return undefined;
|
|
43
|
-
const
|
|
44
|
-
if (Number.isNaN(int))
|
|
45
|
-
return undefined;
|
|
46
|
-
if (body.length === 6) {
|
|
47
|
-
return { r: (int >>> 16) & 255, g: (int >>> 8) & 255, b: int & 255, a: 1 };
|
|
48
|
-
}
|
|
49
|
-
const c = int >>> 0;
|
|
29
|
+
const c = packed >>> 0;
|
|
50
30
|
return {
|
|
51
31
|
r: (c >>> 24) & 255,
|
|
52
32
|
g: (c >>> 16) & 255,
|
|
@@ -54,17 +34,3 @@ function parseHex(hex) {
|
|
|
54
34
|
a: (c & 255) / 255,
|
|
55
35
|
};
|
|
56
36
|
}
|
|
57
|
-
function parseRgb(str) {
|
|
58
|
-
const match = /^rgba?\(([^)]+)\)$/i.exec(str);
|
|
59
|
-
if (match === null)
|
|
60
|
-
return undefined;
|
|
61
|
-
const parts = match[1].split(',').map(part => Number.parseFloat(part.trim()));
|
|
62
|
-
if (parts.length < 3 || parts.some(n => Number.isNaN(n)))
|
|
63
|
-
return undefined;
|
|
64
|
-
return {
|
|
65
|
-
r: parts[0],
|
|
66
|
-
g: parts[1],
|
|
67
|
-
b: parts[2],
|
|
68
|
-
a: parts.length >= 4 ? parts[3] : 1,
|
|
69
|
-
};
|
|
70
|
-
}
|
package/build/events/index.js
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
// node it started on (or a descendant). Bubbling events walk target -> root,
|
|
6
6
|
// invoking each ancestor's listener until one calls stopPropagation. Layout is a
|
|
7
7
|
// direct event in RN and is delivered only to its own target.
|
|
8
|
-
import { dlog } from '../debug.js';
|
|
8
|
+
import { dlog, isDebug } from '../debug.js';
|
|
9
9
|
import { runWrapped } from '../dispatch.js';
|
|
10
10
|
import { getSlot } from '../fabric.js';
|
|
11
11
|
import { isAnchor, isSymbioteNode, } from '../node.js';
|
|
12
12
|
import { registeredNativeEvent } from '../registry.js';
|
|
13
|
+
import { ancestorsOf, parentOf } from '../host-access.js';
|
|
14
|
+
import { setIsJSResponder } from '../imperative.js';
|
|
13
15
|
import { attachTouchHistory, recordTouchTrack, resetTouchHistory, touchHistory, } from '../touch-history.js';
|
|
14
16
|
import { isRecord } from '../type-guards.js';
|
|
15
17
|
// Raw Fabric event -> listener name, split by dispatch phase. Press is synthesized
|
|
@@ -168,7 +170,7 @@ function hasRemainingTouchWithin(owner, nativeEvent) {
|
|
|
168
170
|
// Whether any node from `target` up to the root listens for `listenerName`, used to
|
|
169
171
|
// arm the long-press timer only when a handler would actually receive it.
|
|
170
172
|
function hasListenerInPath(target, listenerName) {
|
|
171
|
-
for (let node = target; node; node = node
|
|
173
|
+
for (let node = target; node; node = parentOf(node)) {
|
|
172
174
|
if (node.listeners?.has(listenerName) === true)
|
|
173
175
|
return true;
|
|
174
176
|
}
|
|
@@ -190,17 +192,20 @@ function callOwnListener(node, listenerName, nativeEvent) {
|
|
|
190
192
|
}
|
|
191
193
|
// The node chain from `from` up to the root, deepest first. The single allocation
|
|
192
194
|
// the two-phase walk indexes both ways (capture reads it reversed).
|
|
195
|
+
//
|
|
196
|
+
// ONE CROSSING, not one per level. This used to climb `parentOf`, which crosses the host boundary
|
|
197
|
+
// each step — 18 questions per event on a depth-8 chain, and again on every frame of every drag for
|
|
198
|
+
// the responder's scope. The host holds the tree, so the chain is one answer it can give: the same
|
|
199
|
+
// argument `parentsOf` / `subtreesOf` won for the teardown sweep (F-2) and `nextSiblingOf` for Vue's
|
|
200
|
+
// quadratic (F-19).
|
|
193
201
|
function pathToRoot(from) {
|
|
194
|
-
|
|
195
|
-
for (let node = from; node; node = node.parent)
|
|
196
|
-
path.push(node);
|
|
197
|
-
return path;
|
|
202
|
+
return [...ancestorsOf(from)];
|
|
198
203
|
}
|
|
199
204
|
// Depth of a node below the root (root = 0). Aligns two nodes before the lockstep
|
|
200
205
|
// climb to their lowest common ancestor.
|
|
201
206
|
function depthOf(node) {
|
|
202
207
|
let depth = 0;
|
|
203
|
-
for (let n = node
|
|
208
|
+
for (let n = parentOf(node); n; n = parentOf(n))
|
|
204
209
|
depth++;
|
|
205
210
|
return depth;
|
|
206
211
|
}
|
|
@@ -213,18 +218,18 @@ function lowestCommonAncestor(a, b) {
|
|
|
213
218
|
let na = a;
|
|
214
219
|
let nb = b;
|
|
215
220
|
while (na && da > db) {
|
|
216
|
-
na = na
|
|
221
|
+
na = parentOf(na);
|
|
217
222
|
da--;
|
|
218
223
|
}
|
|
219
224
|
while (nb && db > da) {
|
|
220
|
-
nb = nb
|
|
225
|
+
nb = parentOf(nb);
|
|
221
226
|
db--;
|
|
222
227
|
}
|
|
223
228
|
while (na && nb) {
|
|
224
229
|
if (na === nb)
|
|
225
230
|
return na;
|
|
226
|
-
na = na
|
|
227
|
-
nb = nb
|
|
231
|
+
na = parentOf(na);
|
|
232
|
+
nb = parentOf(nb);
|
|
228
233
|
}
|
|
229
234
|
return undefined;
|
|
230
235
|
}
|
|
@@ -248,6 +253,33 @@ function findWantsResponder(path, captureName, bubbleName, nativeEvent, skip) {
|
|
|
248
253
|
}
|
|
249
254
|
return undefined;
|
|
250
255
|
}
|
|
256
|
+
// Tell native which node owns the gesture, RN's `injectGlobalResponderHandler`
|
|
257
|
+
// (ReactFabric-dev.js:18862) — the OLD owner first, then the new one, both in one call so no
|
|
258
|
+
// site can do half of it.
|
|
259
|
+
//
|
|
260
|
+
// WITHOUT THIS A JS RESPONDER LOSES TO ANY SCROLL VIEW ABOVE IT, and it is invisible from JS:
|
|
261
|
+
// `onStartShouldSetResponder` returns true, the native UIScrollView never learns the gesture was
|
|
262
|
+
// claimed, and every subsequent move arrives as `topScroll` instead of `topTouchMove` — so the
|
|
263
|
+
// negotiation never even gets a move to grant on. Device-diagnosed 2026-09-08 on a PanResponder
|
|
264
|
+
// drag box inside the canary's ScrollView: `startShouldSet -> true` followed by
|
|
265
|
+
// `topScrollBeginDrag` and twenty `topScroll`, with no grant and no move.
|
|
266
|
+
//
|
|
267
|
+
// `blockNativeResponder` is the taker's own `onResponderGrant` return, exactly as RN reads it.
|
|
268
|
+
function handOverNativeResponder(from, to, blockNativeResponder) {
|
|
269
|
+
if (isDebug()) {
|
|
270
|
+
dlog(`setIsJSResponder from=${from === undefined ? 'none' : 'yes'} ` +
|
|
271
|
+
`to=${to === undefined ? 'none' : 'yes'} block=${blockNativeResponder}`);
|
|
272
|
+
}
|
|
273
|
+
if (from !== undefined)
|
|
274
|
+
setIsJSResponder(from, false, blockNativeResponder);
|
|
275
|
+
if (to !== undefined)
|
|
276
|
+
setIsJSResponder(to, true, blockNativeResponder);
|
|
277
|
+
}
|
|
278
|
+
// Whether the taker asked native to stand down. RN reads this off the grant dispatch's return;
|
|
279
|
+
// an absent listener means no claim, which is RN's `blockNativeResponder || false`.
|
|
280
|
+
function blocksNative(result) {
|
|
281
|
+
return result === true;
|
|
282
|
+
}
|
|
251
283
|
// Negotiate (or re-negotiate) the responder for a touch start/move. If nobody holds
|
|
252
284
|
// it, the winner is granted. If someone does, the incumbent is asked to relinquish
|
|
253
285
|
// via onResponderTerminationRequest (absent listener = implicit yes); on yes it is
|
|
@@ -258,22 +290,56 @@ function negotiateResponder(target, phase, nativeEvent) {
|
|
|
258
290
|
// the responder) and skips the deepest node when it IS the responder (Responder-
|
|
259
291
|
// EventPlugin.setResponderAndExtractTransfer). At touch start currentResponder is
|
|
260
292
|
// cleared, so this collapses to the plain target->root start walk.
|
|
293
|
+
//
|
|
294
|
+
// ONE WALK, and the common case never needs a second. `pathToRoot` crosses the host boundary per
|
|
295
|
+
// level, and the previous shape paid `lowestCommonAncestor` — `depthOf(a)` plus `depthOf(b)` plus
|
|
296
|
+
// a lockstep climb — BEFORE walking again from the answer. Measured on a depth-8 chain: 19
|
|
297
|
+
// crossings per move, of which 18 were the ancestor search, on a path that runs 60 times a second
|
|
298
|
+
// for the whole of a drag (`__tests__/touch-crossing-cost.test.ts`).
|
|
299
|
+
//
|
|
300
|
+
// The scope RN computes is the lowest common ancestor of responder and target, and while a finger
|
|
301
|
+
// stays inside the view that claimed it — which is what a drag IS — the responder is an ANCESTOR
|
|
302
|
+
// of the target, so the LCA is the responder itself and it is already in the target's own path.
|
|
303
|
+
// An index lookup answers it with nothing crossed, and the scoped path is that path's suffix.
|
|
304
|
+
// The genuine cross-subtree case (the finger left the responder's subtree) still falls back to
|
|
305
|
+
// the full search, and it is a rare frame rather than every frame.
|
|
306
|
+
const targetPath = pathToRoot(target);
|
|
307
|
+
const heldAt = currentResponder === undefined ? -1 : targetPath.indexOf(currentResponder);
|
|
261
308
|
const from = currentResponder === undefined
|
|
262
309
|
? target
|
|
263
|
-
:
|
|
310
|
+
: heldAt >= 0
|
|
311
|
+
? currentResponder
|
|
312
|
+
: lowestCommonAncestor(currentResponder, target);
|
|
264
313
|
if (!from)
|
|
265
314
|
return;
|
|
266
|
-
const path =
|
|
315
|
+
const path = from === target
|
|
316
|
+
? targetPath
|
|
317
|
+
: heldAt >= 0
|
|
318
|
+
? targetPath.slice(heldAt)
|
|
319
|
+
: pathToRoot(from);
|
|
267
320
|
const skip = from === currentResponder ? from : undefined;
|
|
268
321
|
const wants = phase === 'start'
|
|
269
322
|
? findWantsResponder(path, START_SHOULD_SET_CAPTURE, START_SHOULD_SET, nativeEvent, skip)
|
|
270
323
|
: findWantsResponder(path, MOVE_SHOULD_SET_CAPTURE, MOVE_SHOULD_SET, nativeEvent, skip);
|
|
271
|
-
|
|
324
|
+
// Every exit is logged: a negotiation that declines is indistinguishable from one that never
|
|
325
|
+
// ran, and the two have opposite causes.
|
|
326
|
+
if (!wants) {
|
|
327
|
+
if (isDebug()) {
|
|
328
|
+
dlog(`responder ${phase}: nobody wants it (path=${path.length}${skip === undefined ? '' : ', one skipped'})`);
|
|
329
|
+
}
|
|
272
330
|
return;
|
|
331
|
+
}
|
|
332
|
+
if (wants === currentResponder) {
|
|
333
|
+
if (isDebug())
|
|
334
|
+
dlog(`responder ${phase}: ${wants.component} already holds it`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
273
337
|
if (currentResponder === undefined) {
|
|
274
338
|
currentResponder = wants;
|
|
275
|
-
|
|
276
|
-
|
|
339
|
+
if (isDebug())
|
|
340
|
+
dlog(`responder granted to ${wants.component}`);
|
|
341
|
+
const granted = callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
|
|
342
|
+
handOverNativeResponder(undefined, wants, blocksNative(granted));
|
|
277
343
|
return;
|
|
278
344
|
}
|
|
279
345
|
const incumbent = currentResponder;
|
|
@@ -291,13 +357,16 @@ function negotiateResponder(target, phase, nativeEvent) {
|
|
|
291
357
|
// visible no-op event with no behavioral counterpart. We therefore fire grant
|
|
292
358
|
// before terminate on the consent path (matching RN's grant<terminate ordering) and
|
|
293
359
|
// omit it on reject; the consent OUTCOME is unchanged either way.
|
|
294
|
-
|
|
295
|
-
|
|
360
|
+
if (isDebug())
|
|
361
|
+
dlog(`responder transferred ${incumbent.component} -> ${wants.component}`);
|
|
362
|
+
const granted = callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
|
|
296
363
|
callOwnListener(incumbent, RESPONDER_TERMINATE, nativeEvent);
|
|
297
364
|
currentResponder = wants;
|
|
365
|
+
handOverNativeResponder(incumbent, wants, blocksNative(granted));
|
|
298
366
|
}
|
|
299
367
|
else {
|
|
300
|
-
|
|
368
|
+
if (isDebug())
|
|
369
|
+
dlog(`responder takeover of ${incumbent.component} rejected`);
|
|
301
370
|
callOwnListener(wants, RESPONDER_REJECT, nativeEvent);
|
|
302
371
|
}
|
|
303
372
|
}
|
|
@@ -309,7 +378,10 @@ export function installEventHandler() {
|
|
|
309
378
|
if (!isSymbioteNode(instanceHandle))
|
|
310
379
|
return;
|
|
311
380
|
if (topLevelType === TOUCH_START) {
|
|
312
|
-
|
|
381
|
+
// The ternary is gone with the gate: `isSymbioteNode(instanceHandle)` was re-asked here
|
|
382
|
+
// three lines after the early return above already proved it.
|
|
383
|
+
if (isDebug())
|
|
384
|
+
dlog(`event ${TOUCH_START} on ${instanceHandle.component}`);
|
|
313
385
|
// Update the touch bank, then attach it so responder handlers (PanResponder)
|
|
314
386
|
// read each touch's own previous->current delta; RN records before dispatch.
|
|
315
387
|
recordTouchTrack('start', nativeEvent);
|
|
@@ -423,7 +495,8 @@ export function installEventHandler() {
|
|
|
423
495
|
bubble(press.owner, PRESS_OUT, nativeEvent);
|
|
424
496
|
}
|
|
425
497
|
if (!hadActivePress) {
|
|
426
|
-
|
|
498
|
+
if (isDebug())
|
|
499
|
+
dlog(`event ${TOUCH_END} ignored (no matching start)`);
|
|
427
500
|
}
|
|
428
501
|
else if (completedPresses.length === 0) {
|
|
429
502
|
dlog('press retained (another touch remains inside its owner)');
|
|
@@ -432,8 +505,10 @@ export function installEventHandler() {
|
|
|
432
505
|
// release) only when the last responder touch lifted.
|
|
433
506
|
if (responder) {
|
|
434
507
|
callOwnListener(responder, RESPONDER_END, nativeEvent);
|
|
435
|
-
if (releases)
|
|
508
|
+
if (releases) {
|
|
436
509
|
callOwnListener(responder, RESPONDER_RELEASE, nativeEvent);
|
|
510
|
+
handOverNativeResponder(responder, undefined, false);
|
|
511
|
+
}
|
|
437
512
|
else
|
|
438
513
|
dlog('responderEnd without release (touches remain inside responder)');
|
|
439
514
|
}
|
|
@@ -468,8 +543,10 @@ export function installEventHandler() {
|
|
|
468
543
|
// Like touch-end, every finger leaving emits responderEnd. Termination is final only
|
|
469
544
|
// when no touch remains inside the responder.
|
|
470
545
|
callOwnListener(responder, RESPONDER_END, nativeEvent);
|
|
471
|
-
if (terminatesResponder)
|
|
546
|
+
if (terminatesResponder) {
|
|
472
547
|
callOwnListener(responder, RESPONDER_TERMINATE, nativeEvent);
|
|
548
|
+
handOverNativeResponder(responder, undefined, false);
|
|
549
|
+
}
|
|
473
550
|
}
|
|
474
551
|
});
|
|
475
552
|
if (touchHistory.numberActiveTouches === 0)
|
|
@@ -478,13 +555,17 @@ export function installEventHandler() {
|
|
|
478
555
|
}
|
|
479
556
|
const direct = DIRECT_EVENTS[topLevelType];
|
|
480
557
|
if (direct !== undefined) {
|
|
481
|
-
|
|
558
|
+
if (isDebug()) {
|
|
559
|
+
dlog(`event ${topLevelType} -> ${direct} (direct)`);
|
|
560
|
+
}
|
|
482
561
|
runWrapped(() => deliverDirect(instanceHandle, direct, nativeEvent));
|
|
483
562
|
return;
|
|
484
563
|
}
|
|
485
564
|
const bubbling = BUBBLING_EVENTS[topLevelType];
|
|
486
565
|
if (bubbling !== undefined) {
|
|
487
|
-
|
|
566
|
+
if (isDebug()) {
|
|
567
|
+
dlog(`event ${topLevelType} -> ${bubbling} (bubble)`);
|
|
568
|
+
}
|
|
488
569
|
runWrapped(() => bubble(instanceHandle, bubbling, nativeEvent));
|
|
489
570
|
return;
|
|
490
571
|
}
|
|
@@ -494,8 +575,10 @@ export function installEventHandler() {
|
|
|
494
575
|
// bubble: same split as the built-ins.
|
|
495
576
|
const registered = registeredNativeEvent(instanceHandle.component, topLevelType);
|
|
496
577
|
if (registered !== undefined) {
|
|
497
|
-
|
|
498
|
-
|
|
578
|
+
if (isDebug()) {
|
|
579
|
+
const phase = registered.direct ? 'direct' : 'bubble';
|
|
580
|
+
dlog(`event ${topLevelType} -> ${registered.listener} (${phase}, registered)`);
|
|
581
|
+
}
|
|
499
582
|
runWrapped(() => registered.direct
|
|
500
583
|
? deliverDirect(instanceHandle, registered.listener, nativeEvent)
|
|
501
584
|
: bubble(instanceHandle, registered.listener, nativeEvent));
|
|
@@ -505,7 +588,9 @@ export function installEventHandler() {
|
|
|
505
588
|
// config. A permanent diagnostic seam: if a native view fires something we drop
|
|
506
589
|
// on the floor (an event the ViewConfig didn't surface, or a name mismatch),
|
|
507
590
|
// this is where it shows up. Keeps "the handler silently did nothing" debuggable.
|
|
508
|
-
|
|
591
|
+
if (isDebug()) {
|
|
592
|
+
dlog(`event ${topLevelType} UNMATCHED on ${instanceHandle.component} (dropped)`);
|
|
593
|
+
}
|
|
509
594
|
});
|
|
510
595
|
}
|
|
511
596
|
// A press is honest only if the touch ends on the node it started on, or a
|
|
@@ -517,7 +602,7 @@ function endsWithin(endTarget, start) {
|
|
|
517
602
|
while (node) {
|
|
518
603
|
if (node === start)
|
|
519
604
|
return true;
|
|
520
|
-
node = node
|
|
605
|
+
node = parentOf(node);
|
|
521
606
|
}
|
|
522
607
|
return false;
|
|
523
608
|
}
|
|
@@ -549,7 +634,8 @@ function bubble(target, listenerName, nativeEvent) {
|
|
|
549
634
|
? undefined
|
|
550
635
|
: node.listeners?.get(captureName);
|
|
551
636
|
if (listener) {
|
|
552
|
-
|
|
637
|
+
if (isDebug())
|
|
638
|
+
dlog(`event ${listenerName} capture on ${node.component}`);
|
|
553
639
|
listener({
|
|
554
640
|
type: listenerName,
|
|
555
641
|
target,
|
|
@@ -563,8 +649,13 @@ function bubble(target, listenerName, nativeEvent) {
|
|
|
563
649
|
}
|
|
564
650
|
// Bubble phase: target -> root, invoking each ancestor's plain listener. Anchors are
|
|
565
651
|
// transparent here too, same reason (see the capture-phase comment above).
|
|
566
|
-
|
|
567
|
-
|
|
652
|
+
//
|
|
653
|
+
// THE SAME ARRAY, read forward. This used to re-walk `parentOf` from the target, which is a
|
|
654
|
+
// CROSSING of the host boundary per level — so one event asked the host for its ancestors twice,
|
|
655
|
+
// measured at 18 questions on a depth-8 chain where 9 is the floor. The comment above already
|
|
656
|
+
// says the path is built once "without a second allocation"; the second WALK is the expensive
|
|
657
|
+
// half, and it was the one left in. Every touch and every frame of every drag pays it.
|
|
658
|
+
for (const node of path) {
|
|
568
659
|
const listener = isAnchor(node)
|
|
569
660
|
? undefined
|
|
570
661
|
: node.listeners?.get(listenerName);
|
|
@@ -580,7 +671,6 @@ function bubble(target, listenerName, nativeEvent) {
|
|
|
580
671
|
if (stopped)
|
|
581
672
|
return;
|
|
582
673
|
}
|
|
583
|
-
node = node.parent;
|
|
584
674
|
}
|
|
585
675
|
}
|
|
586
676
|
// Direct (non-bubbling) delivery: only the target's own listener fires.
|
package/build/fabric-props.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { IFabricProps } from './fabric';
|
|
2
2
|
import { type ISymbioteNode } from './node';
|
|
3
|
-
export declare function fabricProps(node: ISymbioteNode): IFabricProps;
|
|
3
|
+
export declare function fabricProps(node: ISymbioteNode, nodeProps: Readonly<Record<string, unknown>>): IFabricProps;
|