@symbiote-native/engine 1.3.0 → 1.4.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/accessibility-props.d.ts +0 -11
- package/build/accessibility-props.js +30 -68
- package/build/animated/graph.js +1 -1
- package/build/animated/leaf-lifecycle.js +2 -2
- package/build/asset-source-resolver.d.ts +2 -0
- package/build/asset-source-resolver.js +13 -0
- package/build/back-handler/index.d.ts +1 -5
- package/build/back-handler/index.js +0 -6
- package/build/debug.js +8 -22
- package/build/dispatch.js +3 -9
- 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/fabric-props.js +74 -179
- package/build/fabric.d.ts +0 -13
- package/build/fabric.js +18 -38
- package/build/host-access.d.ts +1 -128
- package/build/host-access.js +96 -205
- package/build/host-behavior.d.ts +0 -100
- package/build/host-behavior.js +125 -311
- package/build/image-loader.js +10 -23
- package/build/image-source-resolver.js +3 -7
- package/build/image-source-write.d.ts +0 -11
- package/build/image-source-write.js +14 -34
- package/build/imperative.d.ts +2 -28
- package/build/imperative.js +60 -93
- package/build/index.d.ts +6 -2
- package/build/index.js +29 -39
- package/build/mutation-buffer.d.ts +3 -177
- package/build/mutation-buffer.js +162 -316
- package/build/native-engine.d.ts +6 -102
- package/build/native-engine.js +60 -141
- package/build/native-events.js +9 -18
- package/build/native-tree-host.d.ts +0 -21
- package/build/native-tree-host.js +15 -31
- 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 -309
- package/build/node.js +9 -1564
- package/build/post-commit.js +3 -8
- package/build/process-aspect-ratio.js +3 -7
- package/build/process-background-longhands.js +10 -19
- package/build/process-filter.js +11 -19
- package/build/process-font-variant.js +3 -7
- package/build/registry.d.ts +0 -33
- package/build/registry.js +22 -57
- package/build/report-error.js +4 -18
- package/build/structured-style.d.ts +0 -9
- package/build/structured-style.js +16 -31
- package/build/styles.js +3 -6
- package/build/surface.d.ts +0 -26
- package/build/surface.js +29 -76
- package/build/text-input-state.js +4 -8
- package/build/touch-history.js +5 -11
- package/build/tree-host.d.ts +7 -270
- package/build/tree-host.js +63 -153
- package/build/view-config.js +17 -37
- 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,145 @@
|
|
|
1
|
+
// The listener channel: which `on*` prop becomes a listener, who owns the slot, and the handful of
|
|
2
|
+
// events Fabric gates behind a boolean prop
|
|
3
|
+
import { recordSetOwnedListener } from './mutation-buffer.js';
|
|
4
|
+
import { appListenerFor, hasHostBehaviors, notifyOwnedListenerChange, ownsListener, stashAppListener, } from './host-behavior.js';
|
|
5
|
+
import { IMAGE_LOAD_EVENT_NAMES, anyImageLoadEventListenerWired, } from './image-source-write.js';
|
|
6
|
+
import { setProp } from './node-props.js';
|
|
7
|
+
// Fabric gates a handful of events behind a BOOLEAN prop: unlike scroll/touch/change these fire
|
|
8
|
+
// only when the shadow node carries the flag, and a gated handler would otherwise attach on our
|
|
9
|
+
// side while the native event silently never arrives
|
|
10
|
+
// `magicTap` maps to `onMagicTap`, not the C++ member name `onAccessibilityMagicTap`: RN's own
|
|
11
|
+
// `BaseViewConfig.ios.js` disagrees with its C++ prop name, and matching stock is the only choice
|
|
12
|
+
const GATED_EVENT_PROPS = new Map([
|
|
13
|
+
['layout', 'onLayout'],
|
|
14
|
+
['textLayout', 'onTextLayout'],
|
|
15
|
+
['accessibilityTap', 'onAccessibilityTap'],
|
|
16
|
+
['magicTap', 'onMagicTap'],
|
|
17
|
+
['accessibilityEscape', 'onAccessibilityEscape'],
|
|
18
|
+
['accessibilityAction', 'onAccessibilityAction'],
|
|
19
|
+
]);
|
|
20
|
+
// Install a listener the BEHAVIOR owns, bypassing the ownership check, т.к. `setEventListener`
|
|
21
|
+
// diverts an owned name into the stash and that would be circular for the behavior's dispatcher
|
|
22
|
+
// `undefined` removes it, gate flag included: ScrollView takes the owner's `layout` only while
|
|
23
|
+
// something wants it, and a one-way installer would leave `onLayout: true` stuck in the payload
|
|
24
|
+
export function setBehaviorListener(node, name, listener) {
|
|
25
|
+
if (listener === undefined)
|
|
26
|
+
node.listeners?.delete(name);
|
|
27
|
+
else
|
|
28
|
+
(node.listeners ??= new Map()).set(name, listener);
|
|
29
|
+
// The flag is written UNCONDITIONALLY. Writing it only on the FLIP was measured and rejected: it
|
|
30
|
+
// saves 1 KB per item on the two anchor-backed touchables and costs 10 ms on their teardown
|
|
31
|
+
// (`gated-listener-writes.test.ts`, symbiote-perf-measurement §24)
|
|
32
|
+
const flagProp = GATED_EVENT_PROPS.get(name);
|
|
33
|
+
if (flagProp !== undefined)
|
|
34
|
+
setProp(node, flagProp, listener === undefined ? undefined : true);
|
|
35
|
+
}
|
|
36
|
+
// Arm (or release, with `undefined`) the shared handler a behavior dispatches through, instead of
|
|
37
|
+
// installing one closure per name. See `IEventDispatch`
|
|
38
|
+
export function setNodeDispatch(node, dispatch) {
|
|
39
|
+
node.dispatch = dispatch;
|
|
40
|
+
}
|
|
41
|
+
/** Whether anything at all would receive `name` on this node. */
|
|
42
|
+
export function hasListenerFor(node, name) {
|
|
43
|
+
if (node.listeners?.has(name) === true)
|
|
44
|
+
return true;
|
|
45
|
+
return node.dispatch?.names.has(name) === true;
|
|
46
|
+
}
|
|
47
|
+
// The app's own listener first: a behavior owns its names, so the two never collide, and an
|
|
48
|
+
// unarmed node stops at one `Map` probe
|
|
49
|
+
export function listenerFor(node, name) {
|
|
50
|
+
const own = node.listeners?.get(name);
|
|
51
|
+
if (own !== undefined)
|
|
52
|
+
return own;
|
|
53
|
+
const dispatch = node.dispatch;
|
|
54
|
+
if (dispatch === undefined || !dispatch.names.has(name))
|
|
55
|
+
return undefined;
|
|
56
|
+
// ONE closure per delivered event, where a dispatcher per name built seven per mounted node.
|
|
57
|
+
// `name` is bound here rather than read off `event.type`, so a caller building its own event
|
|
58
|
+
// (every test driving a behavior by hand) reaches the same handler
|
|
59
|
+
return event => dispatch.deliver(node, name, event);
|
|
60
|
+
}
|
|
61
|
+
// The unowned names whose presence a platform rule reads
|
|
62
|
+
const PRESSABILITY_NAMES = new Set([
|
|
63
|
+
'press',
|
|
64
|
+
'longPress',
|
|
65
|
+
'startShouldSetResponder',
|
|
66
|
+
]);
|
|
67
|
+
// A name a host behavior OWNS never reaches `node.listeners`: the behavior's dispatcher holds that
|
|
68
|
+
// slot, and the map is single-slot, so the app's callback would evict it with no diagnostic
|
|
69
|
+
function stashOwnedListener(node, name, value, isHandler) {
|
|
70
|
+
// The PRESENCE only, never the identity: a fresh closure nearly every render must not notify
|
|
71
|
+
const wasWired = appListenerFor(node, name) !== undefined;
|
|
72
|
+
stashAppListener(node, name, isHandler ? value : undefined);
|
|
73
|
+
if (wasWired !== isHandler) {
|
|
74
|
+
// The BIT, on the flip only, so a platform rule can resolve a key depending on whether the app
|
|
75
|
+
// wired anything without the closure leaving JS
|
|
76
|
+
recordSetOwnedListener(node, name, isHandler);
|
|
77
|
+
notifyOwnedListenerChange(node, name, isHandler);
|
|
78
|
+
}
|
|
79
|
+
const flagged = GATED_EVENT_PROPS.get(name);
|
|
80
|
+
if (flagged !== undefined)
|
|
81
|
+
setProp(node, flagged, isHandler ? true : undefined);
|
|
82
|
+
}
|
|
83
|
+
// The explicit event channel. Structural adapters (Svelte `addEventListener`, Angular
|
|
84
|
+
// `Renderer2.listen`) call this with an already-known event name, flat-bag adapters reach it
|
|
85
|
+
// through `routeProp`, and a non-function value clears the listener
|
|
86
|
+
export function setEventListener(node, name, value) {
|
|
87
|
+
const isHandler = typeof value === 'function';
|
|
88
|
+
if (hasHostBehaviors() && ownsListener(node, name)) {
|
|
89
|
+
stashOwnedListener(node, name, value, isHandler);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// A plain `<text>` presses through the engine's own synthesis with no behavior owning the names,
|
|
93
|
+
// yet its payload depends on whether it is pressable. Same bit as the owned path, on the flip
|
|
94
|
+
if (PRESSABILITY_NAMES.has(name)) {
|
|
95
|
+
const wasWired = node.listeners?.has(name) === true;
|
|
96
|
+
if (wasWired !== isHandler)
|
|
97
|
+
recordSetOwnedListener(node, name, isHandler);
|
|
98
|
+
}
|
|
99
|
+
if (isHandler) {
|
|
100
|
+
const handler = value;
|
|
101
|
+
const listeners = (node.listeners ??= new Map());
|
|
102
|
+
listeners.set(name, (event) => handler(event));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
node.listeners?.delete(name);
|
|
106
|
+
}
|
|
107
|
+
const flagProp = GATED_EVENT_PROPS.get(name);
|
|
108
|
+
if (flagProp !== undefined)
|
|
109
|
+
setProp(node, flagProp, isHandler ? true : undefined);
|
|
110
|
+
// `onLoad` and friends are real Fabric events on `RCTImageView`, so they land here rather than in
|
|
111
|
+
// `writeProp`. See `image-source-write.ts` for Android's synthesized `shouldNotifyLoadEvents`
|
|
112
|
+
if (node.resolvesImageSources && IMAGE_LOAD_EVENT_NAMES.has(name)) {
|
|
113
|
+
setProp(node, 'shouldNotifyLoadEvents', anyImageLoadEventListenerWired(node.listeners) ? true : undefined);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// `/^on[A-Z]/` spelled out, т.к. this runs on EVERY prop write and a regex costs measurably more
|
|
117
|
+
// than the character reads. 111 is 'o', 110 is 'n', 65-90 is A-Z, and `charCodeAt` past the end
|
|
118
|
+
// answers NaN, which fails every comparison
|
|
119
|
+
export function isOnEventName(key) {
|
|
120
|
+
if (key.charCodeAt(0) !== 111 || key.charCodeAt(1) !== 110)
|
|
121
|
+
return false;
|
|
122
|
+
const third = key.charCodeAt(2);
|
|
123
|
+
return third >= 65 && third <= 90;
|
|
124
|
+
}
|
|
125
|
+
/** `onChange` to `change`. */
|
|
126
|
+
export function listenerName(propName) {
|
|
127
|
+
return propName.charAt(2).toLowerCase() + propName.slice(3);
|
|
128
|
+
}
|
|
129
|
+
// PanResponder's `panHandlers`: a JS-side protocol synthesized from raw touches, not Fabric
|
|
130
|
+
// ViewConfig events, so `isEventFor` never reports them. Treated as listeners on any node so the
|
|
131
|
+
// handlers attach instead of reaching Fabric as dead props
|
|
132
|
+
export const RESPONDER_EVENTS = new Set([
|
|
133
|
+
'startShouldSetResponder',
|
|
134
|
+
'startShouldSetResponderCapture',
|
|
135
|
+
'moveShouldSetResponder',
|
|
136
|
+
'moveShouldSetResponderCapture',
|
|
137
|
+
'responderGrant',
|
|
138
|
+
'responderReject',
|
|
139
|
+
'responderStart',
|
|
140
|
+
'responderMove',
|
|
141
|
+
'responderEnd',
|
|
142
|
+
'responderRelease',
|
|
143
|
+
'responderTerminate',
|
|
144
|
+
'responderTerminationRequest',
|
|
145
|
+
]);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ISymbioteNode } from './node-types';
|
|
2
|
+
export declare function createElement(component: string, isText?: boolean, tag?: string): ISymbioteNode;
|
|
3
|
+
export declare function createRawText(text: string, tag?: string): ISymbioteNode;
|
|
4
|
+
export declare function debugNodeId(node: ISymbioteNode): number;
|
|
5
|
+
export declare function createAnchor(): ISymbioteNode;
|
|
6
|
+
export declare function createVoid(): ISymbioteNode;
|
|
7
|
+
export declare function createSurfaceRoot(): ISymbioteNode;
|
|
8
|
+
export declare function setNodeComponent(node: ISymbioteNode, component: string): void;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The one shape every retained node has, and the factories that mint it
|
|
2
|
+
import { recordCreateAnchor, recordCreateVoid, recordCreateElement, recordCreateRawText, recordSetComponent, recordSetProp, } from './mutation-buffer.js';
|
|
3
|
+
import { attachHostBehavior, hasHostBehaviors, } from './host-behavior.js';
|
|
4
|
+
import { configPayloadFold } from './registry.js';
|
|
5
|
+
import { dlog } from './debug.js';
|
|
6
|
+
// A cycle, deliberately: `imperative.ts` imports this module and the prototype methods below call
|
|
7
|
+
// back into it. Neither touches the other at module-evaluation time, only inside a function body
|
|
8
|
+
import { measure as engineMeasure, measureInWindow as engineMeasureInWindow, measureLayout as engineMeasureLayout, setNativeProps as engineSetNativeProps, dispatchViewCommand, } from './imperative.js';
|
|
9
|
+
import { ANCHOR_COMPONENT, BRAND, RAW_TEXT_COMPONENT, SURFACE_COMPONENT, VOID_COMPONENT, isSymbioteNode, } from './node-types.js';
|
|
10
|
+
const FOCUS_COMMAND = 'focus';
|
|
11
|
+
const BLUR_COMMAND = 'blur';
|
|
12
|
+
// Names and arg order mirror RN's `ScrollViewCommands`
|
|
13
|
+
const SCROLL_TO_COMMAND = 'scrollTo';
|
|
14
|
+
const SCROLL_TO_END_COMMAND = 'scrollToEnd';
|
|
15
|
+
const FLASH_SCROLL_INDICATORS_COMMAND = 'flashScrollIndicators';
|
|
16
|
+
// A class, so the imperative methods share a prototype instead of being allocated per node, and
|
|
17
|
+
// both factories below mint the same hidden class. Fields are `declare`d and assigned in the
|
|
18
|
+
// constructor, the shape V8 and Hermes handle best
|
|
19
|
+
class SymbioteNode {
|
|
20
|
+
constructor(component, isText) {
|
|
21
|
+
// Every field is assigned here, not lazily: present from the constructor, they all keep ONE
|
|
22
|
+
// hidden class for every node
|
|
23
|
+
this[BRAND] = true;
|
|
24
|
+
this.component = component;
|
|
25
|
+
this.isText = isText;
|
|
26
|
+
this.listeners = undefined;
|
|
27
|
+
this.dispatch = undefined;
|
|
28
|
+
this.hasCommitHook = false;
|
|
29
|
+
this.resolvesImageSources = false;
|
|
30
|
+
this.nativeIdWinsOverId = false;
|
|
31
|
+
this.styleParts = undefined;
|
|
32
|
+
this.payloadFold = undefined;
|
|
33
|
+
this.hostBehavior = undefined;
|
|
34
|
+
this.childHost = undefined;
|
|
35
|
+
this.wrapper = undefined;
|
|
36
|
+
this.mayHaveChildren = false;
|
|
37
|
+
this.isTornDown = false;
|
|
38
|
+
// `slotOf` reads this pair on EVERY handle operand of every op, so both must be stable slots.
|
|
39
|
+
// `slotBatch` starts at a value no real batch carries, so an untouched node needs no flag
|
|
40
|
+
this.slot = 0;
|
|
41
|
+
this.slotBatch = 0;
|
|
42
|
+
this.createdBatch = 0;
|
|
43
|
+
}
|
|
44
|
+
measure(callback) {
|
|
45
|
+
engineMeasure(this, callback);
|
|
46
|
+
}
|
|
47
|
+
measureInWindow(callback) {
|
|
48
|
+
engineMeasureInWindow(this, callback);
|
|
49
|
+
}
|
|
50
|
+
measureLayout(relativeToNativeNode, onSuccess, onFail) {
|
|
51
|
+
if (!isSymbioteNode(relativeToNativeNode)) {
|
|
52
|
+
dlog('measureLayout: relative target must be a host ref');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
engineMeasureLayout(this, relativeToNativeNode, onSuccess, onFail);
|
|
56
|
+
}
|
|
57
|
+
setNativeProps(nativeProps) {
|
|
58
|
+
engineSetNativeProps(this, nativeProps);
|
|
59
|
+
}
|
|
60
|
+
focus() {
|
|
61
|
+
dispatchViewCommand(this, FOCUS_COMMAND, []);
|
|
62
|
+
}
|
|
63
|
+
blur() {
|
|
64
|
+
dispatchViewCommand(this, BLUR_COMMAND, []);
|
|
65
|
+
}
|
|
66
|
+
// The defaults live HERE and nowhere else, so `buildScrollViewHandle` delegating here cannot
|
|
67
|
+
// drift on what `scrollTo()` with no argument means
|
|
68
|
+
scrollTo(options) {
|
|
69
|
+
const x = options?.x ?? 0;
|
|
70
|
+
const y = options?.y ?? 0;
|
|
71
|
+
const animated = options?.animated ?? true;
|
|
72
|
+
dlog(`ScrollView.scrollTo x=${x} y=${y} animated=${animated}`);
|
|
73
|
+
dispatchViewCommand(this, SCROLL_TO_COMMAND, [x, y, animated]);
|
|
74
|
+
}
|
|
75
|
+
scrollToEnd(options) {
|
|
76
|
+
const animated = options?.animated ?? true;
|
|
77
|
+
dlog(`ScrollView.scrollToEnd animated=${animated}`);
|
|
78
|
+
dispatchViewCommand(this, SCROLL_TO_END_COMMAND, [animated]);
|
|
79
|
+
}
|
|
80
|
+
flashScrollIndicators() {
|
|
81
|
+
dlog('ScrollView.flashScrollIndicators');
|
|
82
|
+
dispatchViewCommand(this, FLASH_SCROLL_INDICATORS_COMMAND, []);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Mint an element and record its creation. The node object IS the handle: what the ops address,
|
|
86
|
+
// what the host attaches its native node to, and what Fabric hands back as an event target
|
|
87
|
+
export function createElement(component, isText = false,
|
|
88
|
+
// The intrinsic tag this node came from, when it differs from the Fabric view name above. The
|
|
89
|
+
// behavior registry is keyed by tag, so an adapter creating `<pressable>` must hand it over
|
|
90
|
+
tag = component) {
|
|
91
|
+
const node = new SymbioteNode(component, isText);
|
|
92
|
+
// A primitive that commits NO VIEW resolves to the anchor component through `descriptorFor`, and
|
|
93
|
+
// a primitive whose ENTIRE subtree must vanish resolves to the void one the same way
|
|
94
|
+
if (component === ANCHOR_COMPONENT)
|
|
95
|
+
recordCreateAnchor(node);
|
|
96
|
+
else if (component === VOID_COMPONENT)
|
|
97
|
+
recordCreateVoid(node);
|
|
98
|
+
else
|
|
99
|
+
recordCreateElement(node, component, isText, node);
|
|
100
|
+
// Gated on the boolean, not on the `Map`: this runs ~9 000 times per benchmark create, and an app
|
|
101
|
+
// that registers nothing must pay one boolean read rather than a hash lookup per node
|
|
102
|
+
if (hasHostBehaviors())
|
|
103
|
+
attachHostBehavior(node, tag);
|
|
104
|
+
// A third-party view's own ViewConfig processors, as a fold, AFTER the behavior's: the behavior
|
|
105
|
+
// rewrites wrapper-body props and `validAttributes[*].process` then converts what it produced
|
|
106
|
+
const configFold = configPayloadFold(component);
|
|
107
|
+
if (configFold !== undefined) {
|
|
108
|
+
const behaviorFold = node.payloadFold;
|
|
109
|
+
node.payloadFold =
|
|
110
|
+
behaviorFold === undefined
|
|
111
|
+
? configFold
|
|
112
|
+
: props => configFold(behaviorFold(props));
|
|
113
|
+
}
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
// `tag` mirrors `createElement`'s, т.к. a raw text's CONTENT can still be a function of the
|
|
117
|
+
// platform (Button renders its title uppercased on Android) even with no props an app can write
|
|
118
|
+
export function createRawText(text, tag = RAW_TEXT_COMPONENT) {
|
|
119
|
+
const node = new SymbioteNode(RAW_TEXT_COMPONENT, false);
|
|
120
|
+
recordCreateRawText(node, text);
|
|
121
|
+
// TAG check first, not `hasHostBehaviors()`: almost no raw text is tagged, so an untagged one
|
|
122
|
+
// pays a pointer-equality compare against the default rather than a registry lookup
|
|
123
|
+
if (tag !== RAW_TEXT_COMPONENT && hasHostBehaviors())
|
|
124
|
+
attachHostBehavior(node, tag);
|
|
125
|
+
return node;
|
|
126
|
+
}
|
|
127
|
+
// A `WeakMap` cannot be logged, so this gives every node a small human-readable id assigned lazily,
|
|
128
|
+
// which lets a `dlog` at ref-attach time and one at dispatch time prove they are the SAME object
|
|
129
|
+
const debugIds = new WeakMap();
|
|
130
|
+
let nextDebugId = 1;
|
|
131
|
+
export function debugNodeId(node) {
|
|
132
|
+
let id = debugIds.get(node);
|
|
133
|
+
if (id === undefined) {
|
|
134
|
+
id = nextDebugId++;
|
|
135
|
+
debugIds.set(node, id);
|
|
136
|
+
}
|
|
137
|
+
return id;
|
|
138
|
+
}
|
|
139
|
+
export function createAnchor() {
|
|
140
|
+
const node = new SymbioteNode(ANCHOR_COMPONENT, false);
|
|
141
|
+
recordCreateAnchor(node);
|
|
142
|
+
return node;
|
|
143
|
+
}
|
|
144
|
+
export function createVoid() {
|
|
145
|
+
const node = new SymbioteNode(VOID_COMPONENT, false);
|
|
146
|
+
recordCreateVoid(node);
|
|
147
|
+
return node;
|
|
148
|
+
}
|
|
149
|
+
// One persistent root view per surface, mirroring RN's own `AppContainer`. Not decoration: without
|
|
150
|
+
// `flex: 1` a non-flex root collapses to content height, and without `box-none` a touch outside the
|
|
151
|
+
// app has no escape
|
|
152
|
+
export function createSurfaceRoot() {
|
|
153
|
+
const node = new SymbioteNode(SURFACE_COMPONENT, false);
|
|
154
|
+
recordCreateElement(node, 'RCTView', false, node);
|
|
155
|
+
// Recorded straight, not through `routeProp`: these are literal Fabric props, not props an app
|
|
156
|
+
// authored, so they want none of the class merging or event routing that path exists for
|
|
157
|
+
recordSetProp(node, 'style', { flex: 1 });
|
|
158
|
+
recordSetProp(node, 'pointerEvents', 'box-none');
|
|
159
|
+
return node;
|
|
160
|
+
}
|
|
161
|
+
// Change which Fabric view a node commits as, keeping the node's identity. Both the JS field and
|
|
162
|
+
// the op move, т.к. no prop write moves a node between views and the host must re-create it
|
|
163
|
+
export function setNodeComponent(node, component) {
|
|
164
|
+
if (node.component === component)
|
|
165
|
+
return;
|
|
166
|
+
node.component = component;
|
|
167
|
+
recordSetComponent(node, component);
|
|
168
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ISymbioteNode } from './node-types';
|
|
2
|
+
export declare function takePropStats(): {
|
|
3
|
+
writes: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function takePropKeyTally(): ReadonlyMap<string, number>;
|
|
6
|
+
export declare const REACT_JSX_DEV_PROPS: ReadonlySet<string>;
|
|
7
|
+
export declare function setProp(node: ISymbioteNode, key: string, value: unknown): void;
|
|
8
|
+
export declare function writeProp(node: ISymbioteNode, key: string, value: unknown): void;
|
|
9
|
+
/** What `propOf` consults before asking the host, `undefined` when nothing was stashed. */
|
|
10
|
+
export declare function functionPropOf(node: ISymbioteNode, key: string): unknown;
|
|
11
|
+
export declare function functionPropsOf(node: ISymbioteNode): ReadonlyMap<string, unknown> | undefined;
|
|
12
|
+
export declare function markPropsDirty(node: ISymbioteNode): void;
|
|
13
|
+
export declare function setText(node: ISymbioteNode, text: string): void;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// The prop write path: the one choke point every writer passes on its way to an op, plus the
|
|
2
|
+
// function-prop stash and the two counters `readCommitProfile` drains
|
|
3
|
+
import { isPendingCreate, noteHostSideChange, recordSetProp, recordSetText, } from './mutation-buffer.js';
|
|
4
|
+
import { derivedNodesOf, noteCommitHookNodeChanged, slotDerivesFrom, } from './host-behavior.js';
|
|
5
|
+
import { resolveStructuredStyle } from './structured-style.js';
|
|
6
|
+
import { IMAGE_SOURCE_PROPS, resolveImageSourceProp, } from './image-source-write.js';
|
|
7
|
+
import { Platform } from './platform';
|
|
8
|
+
import { isDebug } from './debug.js';
|
|
9
|
+
import { flushOps, treeHost } from './tree-host.js';
|
|
10
|
+
// How many prop writes an adapter pushed at the engine, read and zeroed through
|
|
11
|
+
// `readCommitProfile`. Not gated behind `isDebug()`, т.к. an integer increment is noise next to the
|
|
12
|
+
// prop write it counts
|
|
13
|
+
const propStats = { writes: 0 };
|
|
14
|
+
export function takePropStats() {
|
|
15
|
+
const snapshot = { writes: propStats.writes };
|
|
16
|
+
propStats.writes = 0;
|
|
17
|
+
return snapshot;
|
|
18
|
+
}
|
|
19
|
+
// `<component>.<key>` to write count, gated behind `isDebug()` т.к. a `Map` lookup per write is
|
|
20
|
+
// real cost on the hottest path. Names which (view, key) pair an aggregate delta is hiding
|
|
21
|
+
let propKeyTally;
|
|
22
|
+
export function takePropKeyTally() {
|
|
23
|
+
const snapshot = propKeyTally ?? new Map();
|
|
24
|
+
propKeyTally = undefined;
|
|
25
|
+
return snapshot;
|
|
26
|
+
}
|
|
27
|
+
// React's JSX dev transform annotates every element with `__self` and `__source`, which React's own
|
|
28
|
+
// host config consumes and never forwards. Both platforms reject them: `__self` is a cyclic module
|
|
29
|
+
// `this`, and `jsi::dynamicFromValue` keeps no visited set
|
|
30
|
+
export const REACT_JSX_DEV_PROPS = new Set([
|
|
31
|
+
'__self',
|
|
32
|
+
'__source',
|
|
33
|
+
]);
|
|
34
|
+
// Function props that never left JS, keyed by node. A function CANNOT cross this wire, т.к.
|
|
35
|
+
// `jsi::dynamicFromValue` THROWS on a callable and kills the whole batch
|
|
36
|
+
const functionProps = new WeakMap();
|
|
37
|
+
// A pure prop set with no event inference: the event-vs-prop decision is `routeProp`'s, never the
|
|
38
|
+
// key's name. `undefined` DELETES the key, where `null` is a legitimate Fabric "reset to default"
|
|
39
|
+
// No `Object.is` dedupe here, т.к. it needs the value the node already holds, which JS does not.
|
|
40
|
+
// The guard lives in the host's `OP_SET_PROP`, where the previous value is a local field
|
|
41
|
+
export function setProp(node, key, value) {
|
|
42
|
+
// A composed primitive's slot, and its wrapper where it has one, can carry a value DERIVED from
|
|
43
|
+
// an owner prop, so `markPropsDirty` bubbles up or neither ever learns
|
|
44
|
+
if (node.childHost !== undefined && slotDerivesFrom(node, key)) {
|
|
45
|
+
markPropsDirty(node.childHost);
|
|
46
|
+
// Past the slot: a `buildStructure` that builds a CHAIN registers the deeper nodes here, and
|
|
47
|
+
// each keeps its own pure fold reading the owner. See `addDerivedNode`
|
|
48
|
+
const derived = derivedNodesOf(node);
|
|
49
|
+
if (derived !== undefined)
|
|
50
|
+
for (const each of derived)
|
|
51
|
+
markPropsDirty(each);
|
|
52
|
+
if (node.wrapper !== undefined)
|
|
53
|
+
markPropsDirty(node.wrapper);
|
|
54
|
+
}
|
|
55
|
+
propStats.writes += 1;
|
|
56
|
+
if (isDebug()) {
|
|
57
|
+
propKeyTally ??= new Map();
|
|
58
|
+
const tallyKey = `${node.component}.${key}`;
|
|
59
|
+
propKeyTally.set(tallyKey, (propKeyTally.get(tallyKey) ?? 0) + 1);
|
|
60
|
+
}
|
|
61
|
+
writeProp(node, key, value);
|
|
62
|
+
}
|
|
63
|
+
// The one place a prop reaches the wire, and the only place that can keep a function off it.
|
|
64
|
+
// `setNativeProps` calls this rather than `recordSetProp`, т.к. it has no `routeProp` in front
|
|
65
|
+
export function writeProp(node, key, value) {
|
|
66
|
+
// Repeated from `routeProp` т.к. THIS is the path with nothing in front of it: `AnimatedProps`
|
|
67
|
+
// re-sends its whole raw bag every frame, so on a JSX adapter `__self` rides straight past
|
|
68
|
+
if (REACT_JSX_DEV_PROPS.has(key))
|
|
69
|
+
return;
|
|
70
|
+
// Arms the node's recurring post-commit hook. HERE т.к. this is where the declarative and
|
|
71
|
+
// `setNativeProps` paths meet, and a hook armed only by the former misses the imperative write
|
|
72
|
+
if (node.hasCommitHook)
|
|
73
|
+
noteCommitHookNodeChanged(node);
|
|
74
|
+
// `boxShadow` / `filter` / `transform` and Image's source props resolve on the way IN, at this
|
|
75
|
+
// same choke point, т.к. the C++ payload builder has no JS to do it headless
|
|
76
|
+
let written = value;
|
|
77
|
+
if (key === 'style' || key === 'activeStyle') {
|
|
78
|
+
written = resolveStructuredStyle(value);
|
|
79
|
+
}
|
|
80
|
+
else if (node.resolvesImageSources && IMAGE_SOURCE_PROPS.has(key)) {
|
|
81
|
+
written = resolveImageSourceProp(value, key === 'source' && Platform.OS === 'android');
|
|
82
|
+
}
|
|
83
|
+
if (typeof written === 'function') {
|
|
84
|
+
let bag = functionProps.get(node);
|
|
85
|
+
if (bag === undefined) {
|
|
86
|
+
bag = new Map();
|
|
87
|
+
functionProps.set(node, bag);
|
|
88
|
+
}
|
|
89
|
+
bag.set(key, value);
|
|
90
|
+
// The host must not be left holding whatever stood under this key before: a stale value read
|
|
91
|
+
// back through `propOf` would beat the function this write just stashed
|
|
92
|
+
recordSetProp(node, key, undefined);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Written over with a non-function, so the stash must let go or it keeps answering
|
|
96
|
+
const bag = functionProps.get(node);
|
|
97
|
+
if (bag !== undefined)
|
|
98
|
+
bag.delete(key);
|
|
99
|
+
recordSetProp(node, key, written);
|
|
100
|
+
}
|
|
101
|
+
/** What `propOf` consults before asking the host, `undefined` when nothing was stashed. */
|
|
102
|
+
export function functionPropOf(node, key) {
|
|
103
|
+
return functionProps.get(node)?.get(key);
|
|
104
|
+
}
|
|
105
|
+
// The same stash, whole, for `propsOf` to layer over the host's answer. `undefined` rather than an
|
|
106
|
+
// empty `Map` for a node that stashed nothing, so the caller hands back the host's own object
|
|
107
|
+
export function functionPropsOf(node) {
|
|
108
|
+
return functionProps.get(node);
|
|
109
|
+
}
|
|
110
|
+
// "Rebuild this node's payload, the fold reads state I just changed". A behavior whose payload is
|
|
111
|
+
// DERIVED has no prop to write, so this is the one route that dirties it directly
|
|
112
|
+
export function markPropsDirty(node) {
|
|
113
|
+
// Announced to the buffer even though it writes no op, т.к. a commit that cannot see this change
|
|
114
|
+
// would skip itself as idle
|
|
115
|
+
noteHostSideChange();
|
|
116
|
+
if (node.hasCommitHook)
|
|
117
|
+
noteCommitHookNodeChanged(node);
|
|
118
|
+
// A node this batch created has no committed payload and the host cannot name it yet, so the mark
|
|
119
|
+
// is a no-op and the `flushOps` it would force is the whole of its cost
|
|
120
|
+
if (isPendingCreate(node))
|
|
121
|
+
return;
|
|
122
|
+
// The host call takes a HANDLE, so it has to be ORDERED after the ops that built the node
|
|
123
|
+
flushOps();
|
|
124
|
+
treeHost()?.markPropsDirty(node);
|
|
125
|
+
}
|
|
126
|
+
// Counted in `propStats`, т.к. a text write IS a prop write, reaching Fabric as `RCTRawText`'s only
|
|
127
|
+
// prop. Unguarded for the same reason `setProp` is: the host holds the standing text and dedupes
|
|
128
|
+
export function setText(node, text) {
|
|
129
|
+
propStats.writes += 1;
|
|
130
|
+
recordSetText(node, text);
|
|
131
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Where every adapter's prop write lands, whatever shape it started in: the slot redirect, the
|
|
2
|
+
// `id` alias, the class/style slots, the event split, and `setProp` for everything else
|
|
3
|
+
import { isEventFor } from './view-config.js';
|
|
4
|
+
import { canonicalClassName, isClassNameValue, resolveClassName, } from './style-registry/index.js';
|
|
5
|
+
import { dlog } from './debug.js';
|
|
6
|
+
import { slotPropNameFor } from './host-behavior.js';
|
|
7
|
+
import { hasAnimatedNodes } from './animated/graph.js';
|
|
8
|
+
import { bindAnimatedEvent, bindAnimatedValue } from './animated/host-binding.js';
|
|
9
|
+
import { REACT_JSX_DEV_PROPS, setProp } from './node-props.js';
|
|
10
|
+
import { isStyleCallback, isSameShallowStyle, pushClassStyle, setNodePressed, stylePartsOf, } from './node-style.js';
|
|
11
|
+
import { RESPONDER_EVENTS, isOnEventName, listenerName, setEventListener, } from './node-events.js';
|
|
12
|
+
const CLASS_PROP_KEYS = new Set(['class', 'className']);
|
|
13
|
+
// `id` is RN's alias for `nativeID` and wins when both are set (`View.js`: `nativeID = id`). No
|
|
14
|
+
// ViewConfig declares raw `id`, so Fabric drops it and a half-working rename loses `nativeID`
|
|
15
|
+
const ID_ALIAS_FROM = 'id';
|
|
16
|
+
const ID_ALIAS_TO = 'nativeID';
|
|
17
|
+
// Precedence needs state т.к. upstream decides `id ?? nativeID` in one expression while a per-key
|
|
18
|
+
// writer never sees both. The authored `nativeID` is remembered, so clearing `id` hands the slot
|
|
19
|
+
// back rather than latching
|
|
20
|
+
const idAliased = new WeakMap();
|
|
21
|
+
function routeIdAlias(node, key, value) {
|
|
22
|
+
const state = idAliased.get(node) ?? {
|
|
23
|
+
idValue: undefined,
|
|
24
|
+
nativeIdValue: undefined,
|
|
25
|
+
};
|
|
26
|
+
if (key === ID_ALIAS_FROM)
|
|
27
|
+
state.idValue = value;
|
|
28
|
+
else
|
|
29
|
+
state.nativeIdValue = value;
|
|
30
|
+
idAliased.set(node, state);
|
|
31
|
+
const published = node.nativeIdWinsOverId
|
|
32
|
+
? (state.nativeIdValue ?? state.idValue)
|
|
33
|
+
: (state.idValue ?? state.nativeIdValue);
|
|
34
|
+
setProp(node, ID_ALIAS_TO, published);
|
|
35
|
+
}
|
|
36
|
+
// A composed primitive's owner receives props that belong to its internal slot
|
|
37
|
+
// (`contentContainerStyle` on ScrollView styles the content view), the same reason the owner is
|
|
38
|
+
// named for a child. Returns whether the write was taken
|
|
39
|
+
function routeToSlot(node, key, value) {
|
|
40
|
+
const slotKey = slotPropNameFor(node, key);
|
|
41
|
+
if (slotKey === undefined)
|
|
42
|
+
return false;
|
|
43
|
+
// A class name is a legal spelling of `contentContainerStyle`, so a string lands on the slot as
|
|
44
|
+
// `class`: renamed verbatim it would publish a style holding a string, dropped with nothing red
|
|
45
|
+
const slotValueFor = node.hostBehavior?.slotValueFor;
|
|
46
|
+
routeProp(node.childHost ?? node, slotKey === 'style' && typeof value === 'string' ? 'class' : slotKey, slotValueFor === undefined ? value : slotValueFor(slotKey, value));
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
// A function `style` arrives intact and is resolved at BOTH states, written as an explicit
|
|
50
|
+
// `style` + `activeStyle` pair. Without this it misses `setEventListener`, lands in `setProp` as a
|
|
51
|
+
// function and is dropped from the payload, so the node commits with no style at all
|
|
52
|
+
function routeStyleValue(node, resolved) {
|
|
53
|
+
const parts = stylePartsOf(node);
|
|
54
|
+
if (isStyleCallback(resolved)) {
|
|
55
|
+
parts.explicitStyle = resolved({ pressed: false });
|
|
56
|
+
parts.activeStyle = resolved({ pressed: true });
|
|
57
|
+
parts.activeStyleFromCallback = true;
|
|
58
|
+
pushClassStyle(node, parts);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Gated on something being published, which keeps the restore path intact, and on the previous
|
|
62
|
+
// write not coming from a callback, т.к. that one owns `parts.activeStyle`
|
|
63
|
+
if (parts.published !== undefined &&
|
|
64
|
+
!parts.activeStyleFromCallback &&
|
|
65
|
+
isSameShallowStyle(resolved, parts.explicitStyle)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
parts.explicitStyle = resolved;
|
|
69
|
+
// Only a variant WE derived is stale now: an AUTHORED `activeStyle` must survive a `style` write,
|
|
70
|
+
// т.к. the two arrive as independent props in an unspecified order
|
|
71
|
+
if (parts.activeStyleFromCallback) {
|
|
72
|
+
parts.activeStyle = undefined;
|
|
73
|
+
parts.activeStyleFromCallback = false;
|
|
74
|
+
}
|
|
75
|
+
pushClassStyle(node, parts);
|
|
76
|
+
}
|
|
77
|
+
// The three keys that reach the style slots rather than the payload. Returns whether one did
|
|
78
|
+
function routeStyleSlot(node, key, resolved) {
|
|
79
|
+
if (CLASS_PROP_KEYS.has(key)) {
|
|
80
|
+
const parts = stylePartsOf(node);
|
|
81
|
+
// Canonicalised HERE so the stored value is what everything downstream keys on: an all-string
|
|
82
|
+
// array becomes one string, and the pressed variant then works on it as on an authored string
|
|
83
|
+
parts.className = canonicalClassName(isClassNameValue(resolved) ? resolved : undefined);
|
|
84
|
+
parts.classStyle = resolveClassName(parts.className);
|
|
85
|
+
pushClassStyle(node, parts);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (key === 'style') {
|
|
89
|
+
routeStyleValue(node, resolved);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
// Ours, never Fabric's: it is consumed here and must not reach the payload
|
|
93
|
+
if (key === 'activeStyle') {
|
|
94
|
+
const parts = stylePartsOf(node);
|
|
95
|
+
parts.activeStyle = resolved;
|
|
96
|
+
// Slot 1 is no longer ours, т.к. whatever a callback derived has just been replaced. Without
|
|
97
|
+
// this the flag outlives its value and a later plain `style` clears a variant we never derived
|
|
98
|
+
parts.activeStyleFromCallback = false;
|
|
99
|
+
pushClassStyle(node, parts);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
// An `on*` name becomes a listener only when the component's ViewConfig declares the event,
|
|
105
|
+
// so `onTintColor` on a Switch routes to `setProp` and reaches Fabric untouched
|
|
106
|
+
function routeEvent(node, key, resolved) {
|
|
107
|
+
// A native-driven `Animated.event` needs the native module as well as the listener map and
|
|
108
|
+
// registers under the PROP name, see `bindAnimatedEvent`
|
|
109
|
+
if (hasAnimatedNodes())
|
|
110
|
+
bindAnimatedEvent(node, key, resolved);
|
|
111
|
+
const name = listenerName(key);
|
|
112
|
+
const isRegisteredEvent = RESPONDER_EVENTS.has(name) || isEventFor(node.component, name);
|
|
113
|
+
// RNS* views derive events from react-native-screens' own codegen ViewConfig, so an unregistered
|
|
114
|
+
// event falls through to `setProp` as a dead prop and looks like "the button did nothing"
|
|
115
|
+
if (node.component.startsWith('RNS')) {
|
|
116
|
+
dlog(`routeProp: ${node.component} "${key}" -> listener "${name}" ` +
|
|
117
|
+
`registered=${isRegisteredEvent} at t=${Date.now()}`);
|
|
118
|
+
}
|
|
119
|
+
if (!isRegisteredEvent)
|
|
120
|
+
return false;
|
|
121
|
+
setEventListener(node, name, resolved);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
export function routeProp(node, key, value) {
|
|
125
|
+
if (REACT_JSX_DEV_PROPS.has(key))
|
|
126
|
+
return;
|
|
127
|
+
// Gated on the field, so a node with no slot pays one load and a branch. Single-hop by
|
|
128
|
+
// construction, т.к. a slot has no slot of its own
|
|
129
|
+
if (node.childHost !== undefined && routeToSlot(node, key, value))
|
|
130
|
+
return;
|
|
131
|
+
// After the slot redirect on purpose: a composed primitive forwards most of its bag to an
|
|
132
|
+
// internal node, so an `id` on the owner belongs there
|
|
133
|
+
if (key === ID_ALIAS_FROM || key === ID_ALIAS_TO) {
|
|
134
|
+
routeIdAlias(node, key, value);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// An AnimatedNode in a prop resolves here to the value to publish, the engine holding the
|
|
138
|
+
// subscription. Returns its input by identity when nothing is animated
|
|
139
|
+
const resolved = hasAnimatedNodes()
|
|
140
|
+
? bindAnimatedValue(node, key, value)
|
|
141
|
+
: value;
|
|
142
|
+
if (routeStyleSlot(node, key, resolved))
|
|
143
|
+
return;
|
|
144
|
+
// RN's snapshot affordance: render the control pressed with no gesture. One string compare on the
|
|
145
|
+
// first commit, where a behavior hook reading it would cost a post-commit crossing per node
|
|
146
|
+
if (key === 'testOnly_pressed')
|
|
147
|
+
setNodePressed(node, resolved === true);
|
|
148
|
+
if (isOnEventName(key) && routeEvent(node, key, resolved))
|
|
149
|
+
return;
|
|
150
|
+
setProp(node, key, resolved);
|
|
151
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type IClassNameValue } from './style-registry';
|
|
2
|
+
import type { IClassStyleParts, ISymbioteNode } from './node-types';
|
|
3
|
+
export declare function isStyleCallback(value: unknown): value is (state: {
|
|
4
|
+
pressed: boolean;
|
|
5
|
+
}) => unknown;
|
|
6
|
+
export declare function stylePartsOf(node: ISymbioteNode): IClassStyleParts;
|
|
7
|
+
export declare function isSameShallowStyle(next: unknown, standing: unknown): boolean;
|
|
8
|
+
export declare function pushClassStyle(node: ISymbioteNode, parts: IClassStyleParts): void;
|
|
9
|
+
export declare function setNodeHidden(node: ISymbioteNode, hidden: boolean): void;
|
|
10
|
+
export declare function setNodePressed(node: ISymbioteNode, pressed: boolean): void;
|
|
11
|
+
export declare function setNodeUnderlayShown(node: ISymbioteNode, shown: boolean): void;
|
|
12
|
+
export declare function clearPublishedStyle(node: ISymbioteNode): void;
|
|
13
|
+
export declare function getExplicitStyle(node: ISymbioteNode): unknown;
|
|
14
|
+
export declare function getPublishedStyle(node: ISymbioteNode): readonly unknown[];
|
|
15
|
+
export type { IClassNameValue };
|