@symbiote-native/engine 1.3.1 → 1.5.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/build/event-value-source.d.ts +4 -0
- package/build/event-value-source.js +3 -0
- package/build/events/delivery.d.ts +9 -0
- package/build/events/delivery.js +143 -0
- package/build/events/index.js +199 -660
- package/build/events/names.d.ts +24 -0
- package/build/events/names.js +80 -0
- package/build/events/press.d.ts +20 -0
- package/build/events/press.js +89 -0
- package/build/events/responder.d.ts +6 -0
- package/build/events/responder.js +124 -0
- package/build/expo-native-view.d.ts +2 -0
- package/build/expo-native-view.js +19 -0
- package/build/imperative.d.ts +2 -0
- package/build/imperative.js +18 -1
- package/build/index.d.ts +12 -2
- package/build/index.js +8 -2
- package/build/mutation-buffer.d.ts +3 -0
- package/build/mutation-buffer.js +25 -8
- package/build/native-engine.d.ts +6 -0
- package/build/native-engine.js +27 -48
- package/build/native-tree-host.js +1 -0
- package/build/node-events.d.ts +11 -0
- package/build/node-events.js +145 -0
- package/build/node-instance.d.ts +8 -0
- package/build/node-instance.js +168 -0
- package/build/node-props.d.ts +13 -0
- package/build/node-props.js +131 -0
- package/build/node-route.d.ts +2 -0
- package/build/node-route.js +151 -0
- package/build/node-style.d.ts +15 -0
- package/build/node-style.js +214 -0
- package/build/node-tree.d.ts +6 -0
- package/build/node-tree.js +159 -0
- package/build/node-types.d.ts +70 -0
- package/build/node-types.js +36 -0
- package/build/node.d.ts +7 -97
- package/build/node.js +9 -1128
- package/build/permission-hook-runtime.d.ts +27 -0
- package/build/permission-hook-runtime.js +37 -0
- package/build/resource-controller.d.ts +6 -0
- package/build/resource-controller.js +30 -0
- package/build/tree-host.d.ts +7 -0
- package/cpp/SymbioteEngineBindings.cpp +19 -18
- package/cpp/SymbioteTree.cpp +81 -156
- package/cpp/SymbioteTree.h +6 -0
- package/package.json +2 -2
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const BUBBLING_EVENTS: Readonly<Record<string, string>>;
|
|
2
|
+
export declare const DIRECT_EVENTS: Readonly<Record<string, string>>;
|
|
3
|
+
export declare const TOUCH_START = "topTouchStart";
|
|
4
|
+
export declare const TOUCH_MOVE = "topTouchMove";
|
|
5
|
+
export declare const TOUCH_END = "topTouchEnd";
|
|
6
|
+
export declare const TOUCH_CANCEL = "topTouchCancel";
|
|
7
|
+
export declare const PRESS = "press";
|
|
8
|
+
export declare const RESPONDER_GRANT = "responderGrant";
|
|
9
|
+
export declare const RESPONDER_REJECT = "responderReject";
|
|
10
|
+
export declare const RESPONDER_START = "responderStart";
|
|
11
|
+
export declare const RESPONDER_MOVE = "responderMove";
|
|
12
|
+
export declare const RESPONDER_END = "responderEnd";
|
|
13
|
+
export declare const RESPONDER_RELEASE = "responderRelease";
|
|
14
|
+
export declare const RESPONDER_TERMINATE = "responderTerminate";
|
|
15
|
+
export declare const RESPONDER_TERMINATION_REQUEST = "responderTerminationRequest";
|
|
16
|
+
export declare const SHOULD_SET_NAMES: Readonly<Record<'start' | 'move', {
|
|
17
|
+
readonly capture: string;
|
|
18
|
+
readonly bubble: string;
|
|
19
|
+
}>>;
|
|
20
|
+
export declare const PRESS_IN = "pressIn";
|
|
21
|
+
export declare const PRESS_OUT = "pressOut";
|
|
22
|
+
export declare const LONG_PRESS = "longPress";
|
|
23
|
+
export declare const DEFAULT_LONG_PRESS_MS = 500;
|
|
24
|
+
export declare const LONG_PRESS_DEACTIVATION_DISTANCE = 10;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Raw Fabric event name -> listener name, and the synthesized names that have no raw event at all.
|
|
2
|
+
// Kept apart from the handler so the file holding the strings is not the file read while debugging
|
|
3
|
+
// a gesture
|
|
4
|
+
// Bubbling events walk target -> root, direct ones fire on the target alone. `press` is synthesized
|
|
5
|
+
// from a touch sequence and `layout` is direct, so neither is in these tables
|
|
6
|
+
export const BUBBLING_EVENTS = {
|
|
7
|
+
topFocus: 'focus',
|
|
8
|
+
topBlur: 'blur',
|
|
9
|
+
topChange: 'change',
|
|
10
|
+
topEndEditing: 'endEditing',
|
|
11
|
+
topSubmitEditing: 'submitEditing',
|
|
12
|
+
topKeyPress: 'keyPress',
|
|
13
|
+
};
|
|
14
|
+
export const DIRECT_EVENTS = {
|
|
15
|
+
topLayout: 'layout',
|
|
16
|
+
topScroll: 'scroll',
|
|
17
|
+
topScrollBeginDrag: 'scrollBeginDrag',
|
|
18
|
+
topScrollEndDrag: 'scrollEndDrag',
|
|
19
|
+
topMomentumScrollBegin: 'momentumScrollBegin',
|
|
20
|
+
topMomentumScrollEnd: 'momentumScrollEnd',
|
|
21
|
+
topSelectionChange: 'selectionChange',
|
|
22
|
+
topContentSizeChange: 'contentSizeChange',
|
|
23
|
+
topLoadStart: 'loadStart',
|
|
24
|
+
topLoad: 'load',
|
|
25
|
+
topLoadEnd: 'loadEnd',
|
|
26
|
+
topError: 'error',
|
|
27
|
+
topProgress: 'progress',
|
|
28
|
+
topPartialLoad: 'partialLoad',
|
|
29
|
+
topRefresh: 'refresh',
|
|
30
|
+
topShow: 'show',
|
|
31
|
+
topRequestClose: 'requestClose',
|
|
32
|
+
topDismiss: 'dismiss',
|
|
33
|
+
topOrientationChange: 'orientationChange',
|
|
34
|
+
topTextLayout: 'textLayout',
|
|
35
|
+
topScrollToTop: 'scrollToTop',
|
|
36
|
+
// From RN's base ViewConfig, so any view may emit them. `accessibilityAction` fires on both
|
|
37
|
+
// platforms; the other three have no Android producer and are inert there
|
|
38
|
+
topAccessibilityAction: 'accessibilityAction',
|
|
39
|
+
topAccessibilityTap: 'accessibilityTap',
|
|
40
|
+
topMagicTap: 'magicTap',
|
|
41
|
+
topAccessibilityEscape: 'accessibilityEscape',
|
|
42
|
+
};
|
|
43
|
+
export const TOUCH_START = 'topTouchStart';
|
|
44
|
+
export const TOUCH_MOVE = 'topTouchMove';
|
|
45
|
+
export const TOUCH_END = 'topTouchEnd';
|
|
46
|
+
export const TOUCH_CANCEL = 'topTouchCancel';
|
|
47
|
+
export const PRESS = 'press';
|
|
48
|
+
// The responder protocol (PanResponder / Touchable), post-`on` names: `onResponderMove` is
|
|
49
|
+
// `responderMove` here. RN's negotiation and its two-phase walk live in `./responder`
|
|
50
|
+
export const RESPONDER_GRANT = 'responderGrant';
|
|
51
|
+
export const RESPONDER_REJECT = 'responderReject';
|
|
52
|
+
export const RESPONDER_START = 'responderStart';
|
|
53
|
+
export const RESPONDER_MOVE = 'responderMove';
|
|
54
|
+
export const RESPONDER_END = 'responderEnd';
|
|
55
|
+
export const RESPONDER_RELEASE = 'responderRelease';
|
|
56
|
+
export const RESPONDER_TERMINATE = 'responderTerminate';
|
|
57
|
+
export const RESPONDER_TERMINATION_REQUEST = 'responderTerminationRequest';
|
|
58
|
+
// The should-set pair asked per phase, as one table т.к. the only thing separating a start
|
|
59
|
+
// negotiation from a move one is which two names get asked
|
|
60
|
+
export const SHOULD_SET_NAMES = {
|
|
61
|
+
start: {
|
|
62
|
+
capture: 'startShouldSetResponderCapture',
|
|
63
|
+
bubble: 'startShouldSetResponder',
|
|
64
|
+
},
|
|
65
|
+
move: {
|
|
66
|
+
capture: 'moveShouldSetResponderCapture',
|
|
67
|
+
bubble: 'moveShouldSetResponder',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
// Synthesized alongside `press` so a Pressable can paint its pressed state. Both fire on the node
|
|
71
|
+
// the touch STARTED on, `pressOut` on end and on cancel
|
|
72
|
+
export const PRESS_IN = 'pressIn';
|
|
73
|
+
export const PRESS_OUT = 'pressOut';
|
|
74
|
+
// Synthesized from a sustained hold, so a bare Text or View gets `onLongPress` with no native
|
|
75
|
+
// event behind it. A fired long press suppresses the tap on release
|
|
76
|
+
export const LONG_PRESS = 'longPress';
|
|
77
|
+
export const DEFAULT_LONG_PRESS_MS = 500;
|
|
78
|
+
// How far the touch may drift before the pending long press is cancelled
|
|
79
|
+
// (`Pressability.DEFAULT_LONG_PRESS_DEACTIVATION_DISTANCE`)
|
|
80
|
+
export const LONG_PRESS_DEACTIVATION_DISTANCE = 10;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ISymbioteNode } from '../node';
|
|
2
|
+
export interface IPressGesture {
|
|
3
|
+
owner: ISymbioteNode;
|
|
4
|
+
longPressTimer: ReturnType<typeof setTimeout> | undefined;
|
|
5
|
+
longPressFired: boolean;
|
|
6
|
+
longPressStart: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
} | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare const activePresses: Set<IPressGesture>;
|
|
12
|
+
export declare function clearLongPress(press: IPressGesture): void;
|
|
13
|
+
export declare function takeAllPresses(): IPressGesture[];
|
|
14
|
+
export declare function findActivePress(target: ISymbioteNode): IPressGesture | undefined;
|
|
15
|
+
export declare function takeEndedPresses(nativeEvent: Record<string, unknown>): IPressGesture[];
|
|
16
|
+
export declare function readTouchPoint(nativeEvent: Record<string, unknown>): {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
} | undefined;
|
|
20
|
+
export declare function hasRemainingTouchWithin(owner: ISymbioteNode, nativeEvent: Record<string, unknown>): boolean;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The synthesized tap: Fabric has no `press` event, so one is built from a touch sequence. This
|
|
2
|
+
// module owns the in-flight gestures and the long-press clock, nothing about delivery
|
|
3
|
+
// A press is INDEPENDENT of the responder: `negotiateResponder` runs on the same touch frames and
|
|
4
|
+
// neither consumes the other, т.к. a View can be a Pressable and a PanResponder target at once
|
|
5
|
+
import { isRecord } from '../type-guards.js';
|
|
6
|
+
import { isSymbioteNode } from '../node.js';
|
|
7
|
+
import { endsWithin } from './delivery.js';
|
|
8
|
+
// Each unrelated target owns an independent press, and further fingers under the same owner JOIN
|
|
9
|
+
// it, so one target still sees exactly one `pressIn` / `press` / `pressOut` lifecycle
|
|
10
|
+
export const activePresses = new Set();
|
|
11
|
+
export function clearLongPress(press) {
|
|
12
|
+
if (press.longPressTimer !== undefined) {
|
|
13
|
+
clearTimeout(press.longPressTimer);
|
|
14
|
+
press.longPressTimer = undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function takeAllPresses() {
|
|
18
|
+
const presses = [...activePresses];
|
|
19
|
+
activePresses.clear();
|
|
20
|
+
for (const press of presses)
|
|
21
|
+
clearLongPress(press);
|
|
22
|
+
return presses;
|
|
23
|
+
}
|
|
24
|
+
export function findActivePress(target) {
|
|
25
|
+
for (const press of activePresses) {
|
|
26
|
+
if (endsWithin(target, press.owner))
|
|
27
|
+
return press;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
// Every press whose last finger left in this frame, removed from the active set. An owner with a
|
|
32
|
+
// finger still down keeps its press, which is what makes a multi-touch lift not a release
|
|
33
|
+
export function takeEndedPresses(nativeEvent) {
|
|
34
|
+
const ended = [];
|
|
35
|
+
for (const press of activePresses) {
|
|
36
|
+
if (hasRemainingTouchWithin(press.owner, nativeEvent))
|
|
37
|
+
continue;
|
|
38
|
+
activePresses.delete(press);
|
|
39
|
+
clearLongPress(press);
|
|
40
|
+
ended.push(press);
|
|
41
|
+
}
|
|
42
|
+
return ended;
|
|
43
|
+
}
|
|
44
|
+
function pagePointOf(source) {
|
|
45
|
+
if (!source)
|
|
46
|
+
return undefined;
|
|
47
|
+
const { pageX, pageY } = source;
|
|
48
|
+
if (typeof pageX === 'number' && typeof pageY === 'number')
|
|
49
|
+
return { x: pageX, y: pageY };
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
// The changed finger's page coordinate, falling back to the active-touch list and then to
|
|
53
|
+
// undefined, which callers read as "skip the coordinate-dependent logic" rather than guessing
|
|
54
|
+
export function readTouchPoint(nativeEvent) {
|
|
55
|
+
const direct = pagePointOf(nativeEvent);
|
|
56
|
+
if (direct)
|
|
57
|
+
return direct;
|
|
58
|
+
// `changedTouches` identifies the finger for THIS frame, so it is read before the full
|
|
59
|
+
// active-touch list, whose first entry may belong to another simultaneously pressed target
|
|
60
|
+
for (const key of ['changedTouches', 'touches']) {
|
|
61
|
+
const touches = nativeEvent[key];
|
|
62
|
+
if (!Array.isArray(touches))
|
|
63
|
+
continue;
|
|
64
|
+
for (const touch of touches) {
|
|
65
|
+
if (!isRecord(touch))
|
|
66
|
+
continue;
|
|
67
|
+
const point = pagePointOf(touch);
|
|
68
|
+
if (point)
|
|
69
|
+
return point;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
// Whether any touch still down started inside `owner`. RN's `noResponderTouches` walks
|
|
75
|
+
// `nativeEvent.touches` the same way; a headless smoke fires an empty `{}`, so no remaining touch,
|
|
76
|
+
// which preserves single-touch behavior
|
|
77
|
+
export function hasRemainingTouchWithin(owner, nativeEvent) {
|
|
78
|
+
const touches = nativeEvent.touches;
|
|
79
|
+
if (!Array.isArray(touches))
|
|
80
|
+
return false;
|
|
81
|
+
for (const touch of touches) {
|
|
82
|
+
if (!isRecord(touch))
|
|
83
|
+
continue;
|
|
84
|
+
const target = touch.target;
|
|
85
|
+
if (isSymbioteNode(target) && endsWithin(target, owner))
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ISymbioteNode } from '../node';
|
|
2
|
+
export declare function heldResponder(): ISymbioteNode | undefined;
|
|
3
|
+
export declare function clearResponder(): void;
|
|
4
|
+
/** Release the responder to native, on a final touch end or a terminate. */
|
|
5
|
+
export declare function releaseResponderToNative(node: ISymbioteNode): void;
|
|
6
|
+
export declare function negotiateResponder(target: ISymbioteNode, phase: 'start' | 'move', nativeEvent: Record<string, unknown>): void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// The responder protocol: who owns the in-flight gesture, and the negotiation that moves it.
|
|
2
|
+
// Split from `./index` so the touch handler reads as four phases rather than as the negotiation
|
|
3
|
+
// `currentResponder` is module state reached from outside through `heldResponder` and
|
|
4
|
+
// `clearResponder`, т.к. the touch handler must read it between its own phases
|
|
5
|
+
import { dlog, isDebug } from '../debug.js';
|
|
6
|
+
import { setIsJSResponder } from '../imperative.js';
|
|
7
|
+
import { hasListenerFor } from '../node.js';
|
|
8
|
+
import { callOwnListener, lowestCommonAncestor, pathToRoot } from './delivery.js';
|
|
9
|
+
import { RESPONDER_GRANT, RESPONDER_REJECT, RESPONDER_TERMINATE, RESPONDER_TERMINATION_REQUEST, SHOULD_SET_NAMES, } from './names.js';
|
|
10
|
+
// The node that claimed the responder for the in-flight touch, or undefined when nobody did.
|
|
11
|
+
// Receives move and release/terminate
|
|
12
|
+
let currentResponder;
|
|
13
|
+
export function heldResponder() {
|
|
14
|
+
return currentResponder;
|
|
15
|
+
}
|
|
16
|
+
export function clearResponder() {
|
|
17
|
+
currentResponder = undefined;
|
|
18
|
+
}
|
|
19
|
+
// RN's two-phase should-set walk: CAPTURE root to deepest, then BUBBLE deepest to root, first true
|
|
20
|
+
// wins. `skip` drops the deepest node when it IS the current responder, т.к. asking the holder to
|
|
21
|
+
// re-claim would let its should-set consume the frame from under its own `onResponderMove`
|
|
22
|
+
function findWantsResponder(path, phase, nativeEvent, skip) {
|
|
23
|
+
const names = SHOULD_SET_NAMES[phase];
|
|
24
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
25
|
+
if (path[i] !== skip &&
|
|
26
|
+
callOwnListener(path[i], names.capture, nativeEvent) === true) {
|
|
27
|
+
return path[i];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const node of path) {
|
|
31
|
+
if (node !== skip &&
|
|
32
|
+
callOwnListener(node, names.bubble, nativeEvent) === true)
|
|
33
|
+
return node;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
// Tell native which node owns the gesture, RN's `injectGlobalResponderHandler`. The OLD owner
|
|
38
|
+
// first, then the new one, both in one call so no site can do half of it
|
|
39
|
+
// WITHOUT THIS A JS RESPONDER LOSES TO ANY SCROLL VIEW ABOVE IT, invisibly: the claim returns true,
|
|
40
|
+
// the native UIScrollView never learns of it, and every later move arrives as `topScroll`
|
|
41
|
+
function handOverNativeResponder(from, to, blockNativeResponder) {
|
|
42
|
+
if (isDebug()) {
|
|
43
|
+
dlog(`setIsJSResponder from=${from === undefined ? 'none' : 'yes'} ` +
|
|
44
|
+
`to=${to === undefined ? 'none' : 'yes'} block=${blockNativeResponder}`);
|
|
45
|
+
}
|
|
46
|
+
if (from !== undefined)
|
|
47
|
+
setIsJSResponder(from, false, blockNativeResponder);
|
|
48
|
+
if (to !== undefined)
|
|
49
|
+
setIsJSResponder(to, true, blockNativeResponder);
|
|
50
|
+
}
|
|
51
|
+
/** Release the responder to native, on a final touch end or a terminate. */
|
|
52
|
+
export function releaseResponderToNative(node) {
|
|
53
|
+
handOverNativeResponder(node, undefined, false);
|
|
54
|
+
}
|
|
55
|
+
// The slice of the target's ancestry the negotiation may ask, plus the node it must not ask
|
|
56
|
+
// ONE WALK, and the common case never needs a second: while a finger stays inside the view that
|
|
57
|
+
// claimed it, the responder IS an ancestor of the target, so the scope RN computes as a lowest
|
|
58
|
+
// common ancestor is already in the target's own path and an index lookup answers it
|
|
59
|
+
function scopeFor(target) {
|
|
60
|
+
const targetPath = pathToRoot(target);
|
|
61
|
+
if (currentResponder === undefined)
|
|
62
|
+
return { path: targetPath, skip: undefined };
|
|
63
|
+
const heldAt = targetPath.indexOf(currentResponder);
|
|
64
|
+
if (heldAt >= 0)
|
|
65
|
+
return { path: targetPath.slice(heldAt), skip: currentResponder };
|
|
66
|
+
// The finger left the responder's subtree, a rare frame rather than every frame
|
|
67
|
+
const from = lowestCommonAncestor(currentResponder, target);
|
|
68
|
+
return { path: from === undefined ? [] : pathToRoot(from), skip: undefined };
|
|
69
|
+
}
|
|
70
|
+
function grantTo(wants, nativeEvent) {
|
|
71
|
+
currentResponder = wants;
|
|
72
|
+
if (isDebug())
|
|
73
|
+
dlog(`responder granted to ${wants.component}`);
|
|
74
|
+
const granted = callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
|
|
75
|
+
handOverNativeResponder(undefined, wants, granted === true);
|
|
76
|
+
}
|
|
77
|
+
// RN's `setResponderAndExtractTransfer` order: grant the TAKER, then terminate the incumbent
|
|
78
|
+
// RN also grants ahead of the termination request purely to read the block-native return. We have
|
|
79
|
+
// no native surface to block, so on the REJECT path that grant is omitted as a visible no-op
|
|
80
|
+
function transferTo(incumbent, wants, nativeEvent) {
|
|
81
|
+
// A missing termination-request listener means implicit consent (RN's default true), so only an
|
|
82
|
+
// explicit non-true answer keeps the incumbent
|
|
83
|
+
const guarded = hasListenerFor(incumbent, RESPONDER_TERMINATION_REQUEST);
|
|
84
|
+
const allowed = !guarded ||
|
|
85
|
+
callOwnListener(incumbent, RESPONDER_TERMINATION_REQUEST, nativeEvent) ===
|
|
86
|
+
true;
|
|
87
|
+
if (!allowed) {
|
|
88
|
+
if (isDebug())
|
|
89
|
+
dlog(`responder takeover of ${incumbent.component} rejected`);
|
|
90
|
+
callOwnListener(wants, RESPONDER_REJECT, nativeEvent);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (isDebug())
|
|
94
|
+
dlog(`responder transferred ${incumbent.component} -> ${wants.component}`);
|
|
95
|
+
const granted = callOwnListener(wants, RESPONDER_GRANT, nativeEvent);
|
|
96
|
+
callOwnListener(incumbent, RESPONDER_TERMINATE, nativeEvent);
|
|
97
|
+
currentResponder = wants;
|
|
98
|
+
handOverNativeResponder(incumbent, wants, granted === true);
|
|
99
|
+
}
|
|
100
|
+
// Negotiate (or re-negotiate) the responder for a touch start or move. With nobody holding it the
|
|
101
|
+
// winner is granted; with an incumbent it is asked to relinquish first
|
|
102
|
+
export function negotiateResponder(target, phase, nativeEvent) {
|
|
103
|
+
const { path, skip } = scopeFor(target);
|
|
104
|
+
if (path.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
const wants = findWantsResponder(path, phase, nativeEvent, skip);
|
|
107
|
+
// Every exit is logged: a negotiation that declines is indistinguishable from one that never
|
|
108
|
+
// ran, and the two have opposite causes
|
|
109
|
+
if (!wants) {
|
|
110
|
+
if (isDebug()) {
|
|
111
|
+
dlog(`responder ${phase}: nobody wants it (path=${path.length}${skip === undefined ? '' : ', one skipped'})`);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (wants === currentResponder) {
|
|
116
|
+
if (isDebug())
|
|
117
|
+
dlog(`responder ${phase}: ${wants.component} already holds it`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (currentResponder === undefined)
|
|
121
|
+
grantTo(wants, nativeEvent);
|
|
122
|
+
else
|
|
123
|
+
transferTo(currentResponder, wants, nativeEvent);
|
|
124
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The Fabric name `requireNativeViewManager` registers: the adapter prefix plus the module name,
|
|
2
|
+
// and the app identifier when the host sets one
|
|
3
|
+
export function expoViewManagerName(moduleName) {
|
|
4
|
+
const appIdentifier = Reflect.get(Reflect.get(globalThis, 'expo') ?? {}, '__expo_app_identifier__');
|
|
5
|
+
const suffix = typeof appIdentifier === 'string' && appIdentifier !== ''
|
|
6
|
+
? `_${appIdentifier}`
|
|
7
|
+
: '';
|
|
8
|
+
return `ViewManagerAdapter_${moduleName}${suffix}`;
|
|
9
|
+
}
|
|
10
|
+
// A missing view config must not crash the render, the view just renders nothing
|
|
11
|
+
export function tryRegisterNativeView(register) {
|
|
12
|
+
try {
|
|
13
|
+
register();
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/build/imperative.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess, IMeasureOnSuccess, IRootTag } from './fabric';
|
|
2
2
|
import { type ISymbioteNode } from './node';
|
|
3
|
+
import { type IDomRect } from './tree-host';
|
|
3
4
|
export declare function registerSurfaceCommit(commit: (rootTag: IRootTag) => void, forget: (rootTag: IRootTag) => void): void;
|
|
4
5
|
export declare function flushNativeProps(): void;
|
|
5
6
|
export declare function requestCommitFor(node: ISymbioteNode): void;
|
|
@@ -14,6 +15,7 @@ export declare function dispatchViewCommand(node: ISymbioteNode, commandName: st
|
|
|
14
15
|
export declare function sendAccessibilityEvent(node: ISymbioteNode, eventType: string): void;
|
|
15
16
|
export declare function measure(node: ISymbioteNode, callback: IMeasureOnSuccess): void;
|
|
16
17
|
export declare function measureInWindow(node: ISymbioteNode, callback: IMeasureInWindowOnSuccess): void;
|
|
18
|
+
export declare function getBoundingClientRect(node: ISymbioteNode, includeTransform: boolean): IDomRect | undefined;
|
|
17
19
|
export declare function measureLayout(node: ISymbioteNode, relativeTo: ISymbioteNode, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void): void;
|
|
18
20
|
export declare function setIsJSResponder(node: ISymbioteNode, isResponder: boolean, blockNativeResponder: boolean): void;
|
|
19
21
|
/** The node's CURRENT prop value, as the host holds it. The behaviors' one read. */
|
package/build/imperative.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { dlog, isDebug } from './debug.js';
|
|
8
8
|
import { clearPublishedStyle, writeProp } from './node.js';
|
|
9
9
|
import { flattenStyle } from './style/index.js';
|
|
10
|
-
import { flushOps, treeHost } from './tree-host.js';
|
|
10
|
+
import { flushOps, treeHost, } from './tree-host.js';
|
|
11
11
|
// What Fabric currently holds for `node`, or undefined before its first commit. Flushes first: an
|
|
12
12
|
// imperative call can land between a mutation and its commit, and the host can't answer about ops
|
|
13
13
|
// it hasn't been handed.
|
|
@@ -136,6 +136,23 @@ export function measureInWindow(node, callback) {
|
|
|
136
136
|
}
|
|
137
137
|
treeHost()?.measureInWindow(node, callback);
|
|
138
138
|
}
|
|
139
|
+
// Synchronous, unlike the six above, mirroring nativeFabricUIManager's own shape. `undefined`
|
|
140
|
+
// covers "not committed", "host predates this member", and a build without react/renderer/dom
|
|
141
|
+
// (the native call throws there) - all read the same to a caller, like a detached element's rect.
|
|
142
|
+
export function getBoundingClientRect(node, includeTransform) {
|
|
143
|
+
const record = committedRecordOf(node);
|
|
144
|
+
if (record === undefined) {
|
|
145
|
+
dlog('getBoundingClientRect skipped: node not committed');
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return treeHost()?.getBoundingClientRect?.(node, includeTransform);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
dlog(`getBoundingClientRect skipped - ${error instanceof Error ? error.message : String(error)}`);
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
139
156
|
// RN's public signature is (relative, onSuccess, onFail) but the native slot wants the fail
|
|
140
157
|
// callback first, so the order is swapped here.
|
|
141
158
|
export function measureLayout(node, relativeTo, onSuccess, onFail = () => { }) {
|
package/build/index.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ export type { ISymbioteNode, ISymbioteEvent, IListener } from './node';
|
|
|
13
13
|
export { SymbioteSurface, createSurface } from './surface';
|
|
14
14
|
export { setEventDispatcher } from './dispatch';
|
|
15
15
|
export { setColorProcessor, processColor } from './platform-color';
|
|
16
|
-
export { dispatchViewCommand, sendAccessibilityEvent, setNativeProps, getNativeTag, getNativeNode, whenCommitted, measure, measureInWindow, measureLayout, disposeRoot, requestCommitFor, } from './imperative';
|
|
16
|
+
export { dispatchViewCommand, sendAccessibilityEvent, setNativeProps, getNativeTag, getNativeNode, whenCommitted, measure, measureInWindow, measureLayout, getBoundingClientRect, disposeRoot, requestCommitFor, } from './imperative';
|
|
17
|
+
export type { IDomRect } from './tree-host';
|
|
17
18
|
export { setTreeHost, treeHost, registerBeforeFlush, readCommitProfile, readSurfaceTelemetry, setNativeDebug, takeNativeDebugLog, } from './tree-host';
|
|
18
19
|
export type { ISurfaceTelemetry } from './tree-host';
|
|
19
20
|
export { nativeTreeHost, installNativeTreeHost } from './native-tree-host';
|
|
@@ -51,6 +52,7 @@ export { reportUncaughtError } from './report-error';
|
|
|
51
52
|
export type { IUncaughtErrorInfo } from './report-error';
|
|
52
53
|
export { getNativeModule, getEnforcingNativeModule } from './native-modules';
|
|
53
54
|
export { installDeviceEventHub, NativeEventEmitter, setDeviceEventSource, } from './native-events';
|
|
55
|
+
export type { IEventValueSource } from './event-value-source';
|
|
54
56
|
export type { IEventSubscription, IEventEmitterModule, INativeEventListener, IDeviceEventSource, } from './native-events';
|
|
55
57
|
export { AnimatedNode, AnimatedWithChildren, AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedInterpolation, AnimatedAddition, AnimatedSubtraction, AnimatedMultiplication, AnimatedDivision, AnimatedModulo, AnimatedDiffClamp, AnimatedEvent, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent, attachNativeEvent, attachNativeEventHandler, flushValue, Easing, createNumericInterpolation, checkValidRanges, TimingAnimation, SpringAnimation, DecayAnimation, AnimatedTracking, timing, spring, decay, parallel, sequence, stagger, loop, delay, nativeAnimated, isNativeAnimatedAvailable, AnimatedProps, createAnimatedLeafLifecycle, AnimatedStyle, AnimatedTransform, AnimatedMock, reduceProps, isAnimatedNode, readPassthroughStyle, resolveHostNode, } from './animated';
|
|
56
58
|
export type { IAnimatedLeafLifecycle, IValueXY, IRgbaValue, IColorInput, IEventConfig, IEventListener, IAnimatedEventHandler, INativeEventAttachment, IValueListener, IEasing, IEasingFunction, IInterpolationConfig, IExtrapolateType, IAnimation, IEndCallback, IEndResult, ICompositeAnimation, ITimingConfig, ISpringConfig, IDecayConfig, IParallelConfig, ILoopAnimationConfig, INativeNodeConfig, INativeAnimationConfig, INativeEventMapping, IPlatformConfig, } from './animated';
|
|
@@ -102,4 +104,12 @@ export { hideTransition, STATUS_BAR_MANAGER, ANIMATED_HIDE_TRANSITION, STATIC_HI
|
|
|
102
104
|
export type { IStatusBarProps, IStatusBarStyle, IStatusBarAnimation, IStatusBarImperative, } from './status-bar/shared';
|
|
103
105
|
export { registerHostBehavior, hasHostBehaviors, hostBehaviorFor, clearHostBehaviors, appListenerFor, addDerivedNode, SLOT_DERIVED_ALL, } from './host-behavior';
|
|
104
106
|
export type { IClaimMode, IHostBehavior, IPayloadFold } from './host-behavior';
|
|
105
|
-
export { setBehaviorListener, markPropsDirty } from './node';
|
|
107
|
+
export { setBehaviorListener, markPropsDirty, setNodeDispatch } from './node';
|
|
108
|
+
export { hasListenerFor, listenerFor } from './node';
|
|
109
|
+
export type { IEventDispatch } from './node';
|
|
110
|
+
export { createPermissionApi, resolveInitialPermission, fetchInitialPermission, splitWriteOnlyPermissionOptions, } from './permission-hook-runtime';
|
|
111
|
+
export type { IWriteOnlyPermissionOptions } from './permission-hook-runtime';
|
|
112
|
+
export { expoViewManagerName, tryRegisterNativeView } from './expo-native-view';
|
|
113
|
+
export { createResourceController } from './resource-controller';
|
|
114
|
+
export type { IResourceController } from './resource-controller';
|
|
115
|
+
export type { IPermissionHookBehavior, IPermissionHookMethods, IPermissionHookOptions, IPermissionApi, IPermissionHookResult, } from './permission-hook-runtime';
|
package/build/index.js
CHANGED
|
@@ -40,7 +40,7 @@ export { setEventDispatcher } from './dispatch.js';
|
|
|
40
40
|
// `setColorProcessor` / `processColor` were only RE-exported by the old `commit.ts`; they point at
|
|
41
41
|
// their real home now.
|
|
42
42
|
export { setColorProcessor, processColor } from './platform-color';
|
|
43
|
-
export { dispatchViewCommand, sendAccessibilityEvent, setNativeProps, getNativeTag, getNativeNode, whenCommitted, measure, measureInWindow, measureLayout, disposeRoot, requestCommitFor, } from './imperative.js';
|
|
43
|
+
export { dispatchViewCommand, sendAccessibilityEvent, setNativeProps, getNativeTag, getNativeNode, whenCommitted, measure, measureInWindow, measureLayout, getBoundingClientRect, disposeRoot, requestCommitFor, } from './imperative.js';
|
|
44
44
|
// The tree host: the seam a runtime installs to answer about the tree JS does not hold. `setTreeHost`
|
|
45
45
|
// is what `installFabric()` (@symbiote-native/test-utils) calls with the TypeScript applier.
|
|
46
46
|
export { setTreeHost, treeHost, registerBeforeFlush, readCommitProfile, readSurfaceTelemetry, setNativeDebug, takeNativeDebugLog, } from './tree-host.js';
|
|
@@ -135,4 +135,10 @@ export { registerHostBehavior, hasHostBehaviors, hostBehaviorFor, clearHostBehav
|
|
|
135
135
|
// markPropsDirty is a behavior's only way to say "the fold reads state I just changed" — every
|
|
136
136
|
// other dirtying route goes through a prop write, and a derived-payload behavior has no prop to
|
|
137
137
|
// write. Pair it with requestCommitFor: dirtying is not publishing.
|
|
138
|
-
export { setBehaviorListener, markPropsDirty } from './node.js';
|
|
138
|
+
export { setBehaviorListener, markPropsDirty, setNodeDispatch } from './node.js';
|
|
139
|
+
// The only correct way to ask what would receive an event: a behavior may answer a name through
|
|
140
|
+
// one shared `IEventDispatch` rather than a closure in `node.listeners`
|
|
141
|
+
export { hasListenerFor, listenerFor } from './node.js';
|
|
142
|
+
export { createPermissionApi, resolveInitialPermission, fetchInitialPermission, splitWriteOnlyPermissionOptions, } from './permission-hook-runtime.js';
|
|
143
|
+
export { expoViewManagerName, tryRegisterNativeView } from './expo-native-view.js';
|
|
144
|
+
export { createResourceController } from './resource-controller.js';
|
|
@@ -20,6 +20,7 @@ export declare const KIND_ANCHOR = 2;
|
|
|
20
20
|
export type IMutationHandle = {
|
|
21
21
|
slot: number;
|
|
22
22
|
slotBatch: number;
|
|
23
|
+
createdBatch: number;
|
|
23
24
|
};
|
|
24
25
|
export type IMutationBatch = {
|
|
25
26
|
readonly ops: Int32Array;
|
|
@@ -38,6 +39,8 @@ export declare function noteCommitDrained(): void;
|
|
|
38
39
|
export declare function noteHostSideChange(): void;
|
|
39
40
|
/** Does the pending batch hold anything that could change what the host says this node's parent is? */
|
|
40
41
|
export declare function hasPendingPlacement(handle: IMutationHandle): boolean;
|
|
42
|
+
/** Has the host never heard of this node, т.к. the op that creates it is still in the buffer? */
|
|
43
|
+
export declare function isPendingCreate(handle: IMutationHandle): boolean;
|
|
41
44
|
export declare function recordCreateElement(handle: IMutationHandle, viewName: string, isText: boolean, instanceHandle: unknown): void;
|
|
42
45
|
export declare function recordCreateRawText(handle: IMutationHandle, text: string): void;
|
|
43
46
|
export declare function recordCreateAnchor(handle: IMutationHandle): void;
|
package/build/mutation-buffer.js
CHANGED
|
@@ -188,7 +188,7 @@ function slotOf(handle) {
|
|
|
188
188
|
// OP_COMMIT is excluded deliberately: recording a commit is not a change to the tree, and counting
|
|
189
189
|
// it would make every commit look like it had work.
|
|
190
190
|
let changedSinceCommit = false;
|
|
191
|
-
function push(op, a = 0, b = 0, c = 0
|
|
191
|
+
function push(op, a = 0, b = 0, c = 0) {
|
|
192
192
|
if (op !== OP_COMMIT)
|
|
193
193
|
changedSinceCommit = true;
|
|
194
194
|
if (opCount + OP_STRIDE > ops.length) {
|
|
@@ -200,10 +200,16 @@ function push(op, a = 0, b = 0, c = 0, d = 0, e = 0) {
|
|
|
200
200
|
ops[opCount + 1] = a;
|
|
201
201
|
ops[opCount + 2] = b;
|
|
202
202
|
ops[opCount + 3] = c;
|
|
203
|
-
ops[opCount + 4] =
|
|
204
|
-
ops[opCount + 5] =
|
|
203
|
+
ops[opCount + 4] = 0;
|
|
204
|
+
ops[opCount + 5] = 0;
|
|
205
205
|
opCount += OP_STRIDE;
|
|
206
206
|
}
|
|
207
|
+
// `OP_CREATE_ELEMENT` is the only op with a FOURTH operand, and it writes it itself rather than
|
|
208
|
+
// widening `push` for one call site. A rest parameter or an options bag would allocate per op,
|
|
209
|
+
// which is the one thing this buffer exists to avoid
|
|
210
|
+
function pushFourth(value) {
|
|
211
|
+
ops[opCount - OP_STRIDE + 4] = value;
|
|
212
|
+
}
|
|
207
213
|
// Whether a commit would publish anything. See changedSinceCommit.
|
|
208
214
|
export function hasChangedSinceCommit() {
|
|
209
215
|
return changedSinceCommit;
|
|
@@ -235,21 +241,32 @@ let placementPending = new Set();
|
|
|
235
241
|
export function hasPendingPlacement(handle) {
|
|
236
242
|
return placementPending.has(handle);
|
|
237
243
|
}
|
|
244
|
+
// A field rather than a second Set, т.к. this is read on the create path and a `Set.add` per node
|
|
245
|
+
// would charge the thing it saves. `createdBatch` starts at 0, which no live batch carries
|
|
246
|
+
function noteCreated(handle) {
|
|
247
|
+
placementPending.add(handle);
|
|
248
|
+
handle.createdBatch = batchId;
|
|
249
|
+
}
|
|
250
|
+
/** Has the host never heard of this node, т.к. the op that creates it is still in the buffer? */
|
|
251
|
+
export function isPendingCreate(handle) {
|
|
252
|
+
return handle.createdBatch === batchId;
|
|
253
|
+
}
|
|
238
254
|
export function recordCreateElement(handle, viewName, isText, instanceHandle) {
|
|
239
255
|
instanceHandles.push(instanceHandle);
|
|
240
|
-
|
|
241
|
-
push(OP_CREATE_ELEMENT, slotOf(handle), intern(viewName), isText ? 1 : 0
|
|
256
|
+
noteCreated(handle);
|
|
257
|
+
push(OP_CREATE_ELEMENT, slotOf(handle), intern(viewName), isText ? 1 : 0);
|
|
258
|
+
pushFourth(instanceHandles.length - 1);
|
|
242
259
|
}
|
|
243
260
|
export function recordCreateRawText(handle, text) {
|
|
244
|
-
|
|
261
|
+
noteCreated(handle);
|
|
245
262
|
push(OP_CREATE_RAW_TEXT, slotOf(handle), intern(text));
|
|
246
263
|
}
|
|
247
264
|
export function recordCreateAnchor(handle) {
|
|
248
|
-
|
|
265
|
+
noteCreated(handle);
|
|
249
266
|
push(OP_CREATE_ANCHOR, slotOf(handle));
|
|
250
267
|
}
|
|
251
268
|
export function recordCreateVoid(handle) {
|
|
252
|
-
|
|
269
|
+
noteCreated(handle);
|
|
253
270
|
push(OP_CREATE_VOID, slotOf(handle));
|
|
254
271
|
}
|
|
255
272
|
export function recordAppendChild(parent, child) {
|
package/build/native-engine.d.ts
CHANGED
|
@@ -31,6 +31,12 @@ export type INativeEngineBindings = {
|
|
|
31
31
|
measureInWindow: (handle: object, callback: (x: number, y: number, width: number, height: number) => void) => void;
|
|
32
32
|
measureLayout: (handle: object, relativeTo: object, onFail: () => void, onSuccess: (x: number, y: number, width: number, height: number) => void) => void;
|
|
33
33
|
setIsJSResponder: (handle: object, isResponder: boolean, blockNativeResponder: boolean) => void;
|
|
34
|
+
getBoundingClientRect?: (handle: object, includeTransform: boolean) => {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
} | undefined;
|
|
34
40
|
setDebugEnabled?: (enabled: boolean) => void;
|
|
35
41
|
takeDebugLog?: () => readonly string[];
|
|
36
42
|
readSurfaceTelemetry?: (surfaceId: number) => {
|