@symbiote-native/engine 1.2.0 → 1.3.1
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/android/CMakeLists.txt +31 -1
- package/android/build.gradle +4 -1
- package/build/accessibility-info/index.android.js +21 -17
- package/build/accessibility-info/index.ios.js +2 -0
- package/build/accessibility-info/shared.d.ts +1 -1
- package/build/accessibility-props.d.ts +0 -11
- package/build/accessibility-props.js +30 -68
- package/build/alert/shared.d.ts +1 -1
- package/build/alert/shared.js +3 -7
- package/build/animated/graph.js +1 -1
- package/build/animated/leaf-lifecycle.js +2 -2
- package/build/app-state/index.d.ts +1 -1
- package/build/app-state/index.js +35 -27
- package/build/asset-source-resolver.d.ts +2 -0
- package/build/asset-source-resolver.js +13 -0
- package/build/back-handler/index.d.ts +7 -6
- package/build/back-handler/index.js +19 -16
- package/build/debug.js +8 -22
- package/build/dispatch.js +3 -9
- package/build/events/index.js +12 -3
- package/build/fabric-props.js +75 -180
- package/build/fabric.d.ts +2 -8
- package/build/fabric.js +27 -33
- package/build/host-access.d.ts +1 -128
- package/build/host-access.js +96 -205
- package/build/host-behavior.d.ts +1 -100
- package/build/host-behavior.js +125 -311
- package/build/image-loader.d.ts +4 -2
- package/build/image-loader.js +24 -48
- package/build/image-source-resolver.js +3 -7
- package/build/image-source-write.d.ts +1 -12
- package/build/image-source-write.js +28 -36
- package/build/imperative.d.ts +0 -28
- package/build/imperative.js +42 -92
- package/build/index.d.ts +3 -2
- package/build/index.js +30 -43
- package/build/invariant.d.ts +1 -0
- package/build/invariant.js +10 -0
- package/build/keyboard/index.js +11 -32
- package/build/linking/index.android.js +5 -3
- package/build/linking/shared.d.ts +1 -1
- package/build/linking/shared.js +16 -19
- package/build/mutation-buffer.d.ts +0 -177
- package/build/mutation-buffer.js +137 -308
- package/build/native-engine.d.ts +0 -102
- package/build/native-engine.js +38 -98
- package/build/native-events.d.ts +5 -0
- package/build/native-events.js +30 -18
- package/build/native-tree-host.d.ts +0 -21
- package/build/native-tree-host.js +14 -31
- package/build/node.d.ts +0 -212
- package/build/node.js +354 -774
- package/build/permissions-android/index.android.d.ts +59 -0
- package/build/permissions-android/index.android.js +47 -0
- package/build/permissions-android/index.d.ts +1 -115
- package/build/permissions-android/index.ios.d.ts +59 -0
- package/build/permissions-android/index.ios.js +31 -0
- package/build/permissions-android/index.js +3 -184
- package/build/permissions-android/shared.d.ts +63 -0
- package/build/permissions-android/shared.js +66 -0
- package/build/platform/index.android.js +3 -5
- package/build/platform/index.ios.js +3 -3
- package/build/platform/shared.d.ts +4 -0
- package/build/platform/shared.js +9 -0
- package/build/platform-color/index.android.d.ts +4 -0
- package/build/platform-color/index.android.js +7 -0
- package/build/platform-color/index.d.ts +2 -20
- package/build/platform-color/index.js +1 -45
- package/build/platform-color/shared.d.ts +21 -0
- package/build/platform-color/shared.js +41 -0
- 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/settings/index.android.d.ts +6 -0
- package/build/settings/index.android.js +21 -0
- package/build/settings/index.d.ts +1 -8
- package/build/settings/index.ios.d.ts +8 -0
- package/build/settings/index.ios.js +122 -0
- package/build/settings/index.js +3 -122
- package/build/share/index.android.js +9 -32
- package/build/share/index.ios.js +14 -15
- package/build/share/shared.d.ts +4 -2
- package/build/share/shared.js +7 -10
- package/build/status-bar/index.android.js +1 -1
- package/build/status-bar/index.ios.js +4 -3
- 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/toast-android/index.android.d.ts +10 -0
- package/build/toast-android/index.android.js +108 -0
- package/build/toast-android/index.d.ts +1 -10
- package/build/toast-android/index.ios.d.ts +10 -0
- package/build/toast-android/index.ios.js +19 -0
- package/build/toast-android/index.js +3 -108
- package/build/touch-history.js +5 -11
- package/build/tree-host.d.ts +0 -270
- package/build/tree-host.js +63 -153
- package/build/view-config.js +17 -37
- package/cpp/SymbioteFabricProps.cpp +204 -120
- package/cpp/SymbioteFabricProps.h +5 -0
- package/cpp/SymbioteTree.cpp +20 -6
- package/package.json +2 -2
package/build/node.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
// The mutation API. Adapters call it; every call appends an OPCODE to `mutation-buffer.ts` and
|
|
2
2
|
// nothing else. There is no tree here — no parent, no children, no props, no mirror. Turning the
|
|
3
|
-
// buffer into a tree is the HOST's job (`tree-host.ts`)
|
|
4
|
-
// `@symbiote-native/test-utils` headlessly.
|
|
5
|
-
//
|
|
6
|
-
// What a node still legitimately owns is what the framework, not Fabric, put on it: the Fabric view
|
|
7
|
-
// name it was created as, whether it is a text container, its JS listener map, the declarative
|
|
8
|
-
// class/style halves the engine merges, and the two bookkeeping flags. An ADDRESS plus the state
|
|
9
|
-
// that never crosses.
|
|
3
|
+
// buffer into a tree is the HOST's job (`tree-host.ts`).
|
|
10
4
|
import { recordAppendChild, recordCreateAnchor, recordCreateVoid, noteHostSideChange, recordCreateElement, recordCreateRawText, recordInsertBefore, recordRemoveChild, recordSetComponent, recordSetOwnedListener, recordSetUnderlayShown, recordSetProp, recordSetText, } from './mutation-buffer.js';
|
|
11
5
|
import { isEventFor } from './view-config.js';
|
|
12
6
|
import { canonicalClassName, EMPTY_STYLE, isClassNameValue, resolveActiveClassName, resolveClassName, } from './style-registry/index.js';
|
|
@@ -15,12 +9,10 @@ import { appListenerFor, attachHostBehavior, claimModeFor, hasAttachedBehaviors,
|
|
|
15
9
|
import { configPayloadFold } from './registry.js';
|
|
16
10
|
import { resolveStructuredStyle } from './structured-style.js';
|
|
17
11
|
import { IMAGE_SOURCE_PROPS, IMAGE_LOAD_EVENT_NAMES, anyImageLoadEventListenerWired, resolveImageSourceProp, } from './image-source-write.js';
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// the registration-side-effect shape Metro's inlineRequires silently drops in release builds (see
|
|
23
|
-
// CLAUDE.md, "Never make correctness depend on a module's load-time side effect").
|
|
12
|
+
import { Platform } from './platform';
|
|
13
|
+
// A cycle, deliberately: `imperative.ts` imports this module; the prototype methods below call
|
|
14
|
+
// back into it. Neither touches the other at module-evaluation time, only inside a function body,
|
|
15
|
+
// so every loader resolves it (CLAUDE.md's load-time side-effect rule).
|
|
24
16
|
import { measure as engineMeasure, measureInWindow as engineMeasureInWindow, measureLayout as engineMeasureLayout, setNativeProps as engineSetNativeProps, dispatchViewCommand, } from './imperative.js';
|
|
25
17
|
// The same deliberate cycle, for the same reason: `tree-host.ts` imports `takePropStats` from here
|
|
26
18
|
// and `censusRetainedTree` below asks it for the census. Function bodies only, on both sides.
|
|
@@ -31,11 +23,9 @@ import { EMPTY_CENSUS, flushOps, treeHost, } from './tree-host.js';
|
|
|
31
23
|
import { hasAnimatedNodes } from './animated/graph.js';
|
|
32
24
|
import { bindAnimatedEvent, bindAnimatedValue, hasAnimatedBindings, reattachAnimatedProps, } from './animated/host-binding.js';
|
|
33
25
|
const BRAND = Symbol('symbiote.node');
|
|
34
|
-
// A node carries the Fabric view name directly, so adding a primitive
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// <Text> becomes a virtual span. `isText` marks a text container so its
|
|
38
|
-
// descendants pick the virtual variant.
|
|
26
|
+
// A node carries the Fabric view name directly, so adding a primitive is just a new string from
|
|
27
|
+
// the adapter, no core change. The only name resolved at commit time is text: a <Text> nested
|
|
28
|
+
// inside another <Text> becomes a virtual span; `isText` marks a container so descendants pick it.
|
|
39
29
|
export const RAW_TEXT_COMPONENT = 'RCTRawText';
|
|
40
30
|
export const TEXT_COMPONENT = 'RCTText';
|
|
41
31
|
export const VIRTUAL_TEXT_COMPONENT = 'RCTVirtualText';
|
|
@@ -54,56 +44,31 @@ const BLUR_COMMAND = 'blur';
|
|
|
54
44
|
const SCROLL_TO_COMMAND = 'scrollTo';
|
|
55
45
|
const SCROLL_TO_END_COMMAND = 'scrollToEnd';
|
|
56
46
|
const FLASH_SCROLL_INDICATORS_COMMAND = 'flashScrollIndicators';
|
|
57
|
-
// The one shape every retained node has
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// Fields are `declare`d and assigned in the constructor rather than written as class fields: with
|
|
62
|
-
// ES2022 field semantics the two are equivalent in meaning but not in emit, and a plain
|
|
63
|
-
// constructor assignment is the shape every engine (V8 and Hermes both) handles without a
|
|
64
|
-
// define-per-field.
|
|
47
|
+
// The one shape every retained node has: a class, so the imperative methods share a prototype
|
|
48
|
+
// instead of being allocated per node, and both factories below mint the same hidden class. Fields
|
|
49
|
+
// are `declare`d and assigned in the constructor, the shape V8/Hermes handle best.
|
|
65
50
|
class SymbioteNode {
|
|
66
51
|
constructor(component, isText) {
|
|
52
|
+
// Every field below is assigned here, not lazily: present from the constructor, they all keep
|
|
53
|
+
// ONE hidden class for every node. `attachHostBehavior` raises several of them later for the
|
|
54
|
+
// rare node whose behavior declares that capability.
|
|
67
55
|
this[BRAND] = true;
|
|
68
56
|
this.component = component;
|
|
69
57
|
this.isText = isText;
|
|
70
58
|
this.listeners = undefined;
|
|
71
|
-
// Assigned here, not lazily on first use: every slot present from the constructor keeps one
|
|
72
|
-
// hidden class for every node. Adding one on demand buys a shape transition per node that needs
|
|
73
|
-
// it, which is the opposite of what these fields are for.
|
|
74
|
-
//
|
|
75
|
-
// `attachHostBehavior` raises this a few lines later for the rare node whose behavior declares
|
|
76
|
-
// the recurring hook.
|
|
77
59
|
this.hasCommitHook = false;
|
|
78
|
-
// Same again; `attachHostBehavior` raises it for the one behavior that declares it, Image's.
|
|
79
60
|
this.resolvesImageSources = false;
|
|
80
|
-
// Same again; `attachHostBehavior` raises it for the one behavior that declares it,
|
|
81
|
-
// TouchableWithoutFeedback's.
|
|
82
61
|
this.nativeIdWinsOverId = false;
|
|
83
62
|
this.styleParts = undefined;
|
|
84
|
-
// Assigned here for the same hidden-class reason as `hasAriaAlias` above; `attachHostBehavior`
|
|
85
|
-
// overwrites it a few lines later for the rare node that has a behavior.
|
|
86
63
|
this.payloadFold = undefined;
|
|
87
|
-
// Same reason again, and the same writer: `attachHostBehavior` fills it for the rare node that
|
|
88
|
-
// gets a behavior at all.
|
|
89
64
|
this.hostBehavior = undefined;
|
|
90
|
-
// Same reason again, and here it is load-bearing rather than tidy: the redirect below is read
|
|
91
|
-
// on every append, so the slot must be a stable slot on one hidden class, not a property added
|
|
92
|
-
// to a few nodes after the fact.
|
|
93
65
|
this.childHost = undefined;
|
|
94
66
|
this.wrapper = undefined;
|
|
95
|
-
// Same hidden-class reason as the two above, and here it is the whole point: the fast path it
|
|
96
|
-
// guards is read on every `childrenOf`, so it must be a stable slot rather than a property that
|
|
97
|
-
// appears on some nodes later.
|
|
98
67
|
this.mayHaveChildren = false;
|
|
99
|
-
// Same hidden-class reason again, and the same measured one: every insert reads it and the
|
|
100
|
-
// teardown sweep writes it per node.
|
|
101
68
|
this.isTornDown = false;
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// value no batch ever carries, which is what makes an untouched node read as "not in this
|
|
106
|
-
// batch" without a separate flag.
|
|
69
|
+
// `slotOf` reads this pair on EVERY handle operand of every op, so both must be stable slots.
|
|
70
|
+
// `slotBatch` starts at a value no real batch carries, so an untouched node reads as "not in
|
|
71
|
+
// this batch" with no separate flag.
|
|
107
72
|
this.slot = 0;
|
|
108
73
|
this.slotBatch = 0;
|
|
109
74
|
}
|
|
@@ -149,58 +114,38 @@ class SymbioteNode {
|
|
|
149
114
|
dispatchViewCommand(this, FLASH_SCROLL_INDICATORS_COMMAND, []);
|
|
150
115
|
}
|
|
151
116
|
}
|
|
152
|
-
// The committed record — handle, tag, rootTag — is the host's
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// The identity check `committedOf` used to make is gone with them, and it was worth something: a Vue
|
|
158
|
-
// `reactive()` / deep-`ref()` Proxy around a host element forwards a field read to its target, so a
|
|
159
|
-
// wrapped node used to hand back a real record. It cannot now — the host keys on the handle OBJECT,
|
|
160
|
-
// so a Proxy misses and every imperative call degrades to its "node not committed" log, which is
|
|
161
|
-
// the WeakMap's old behaviour restored. Hold host nodes with `shallowRef` (vue-adapter-reactivity).
|
|
162
|
-
/**
|
|
163
|
-
* Mint an element and record its creation.
|
|
164
|
-
*
|
|
165
|
-
* The node object IS the handle: it is what the ops address, what the host attaches its native node
|
|
166
|
-
* to, and what Fabric hands back as an event target. Nothing else is allocated.
|
|
167
|
-
*/
|
|
117
|
+
// The committed record — handle, tag, rootTag — is the host's; `committedRecordOf` (tree-host.ts)
|
|
118
|
+
// answers for it, keyed on the handle OBJECT. A Vue Proxy around a host element misses that key,
|
|
119
|
+
// so a wrapped node's calls degrade to "not committed" — hold host nodes with `shallowRef`.
|
|
120
|
+
// Mint an element and record its creation. The node object IS the handle: what the ops address,
|
|
121
|
+
// what the host attaches its native node to, what Fabric hands back as an event target.
|
|
168
122
|
export function createElement(component, isText = false,
|
|
169
123
|
// The intrinsic tag this node came from, when it differs from the Fabric view name above. The
|
|
170
|
-
// behavior registry is keyed by tag
|
|
171
|
-
//
|
|
172
|
-
// (host-behavior.ts, `attached`). Nothing is stored — the lookup happens once, right below.
|
|
124
|
+
// behavior registry is keyed by tag, so an adapter creating `<pressable>` must hand it over here
|
|
125
|
+
// or the registration cannot fire. Nothing is stored — the lookup happens once, right below.
|
|
173
126
|
tag = component) {
|
|
174
127
|
const node = new SymbioteNode(component, isText);
|
|
175
128
|
// A primitive that commits NO VIEW resolves to the anchor component through `descriptorFor`
|
|
176
|
-
// (
|
|
177
|
-
//
|
|
178
|
-
// now, not a name the commit walk reads, so the name alone would give the host an ordinary
|
|
179
|
-
// element called `#anchor` — one that really paints.
|
|
129
|
+
// (touchable-without-feedback, touchable-native-feedback), reaching this rather than
|
|
130
|
+
// `createAnchor` because the caller only knows it has a descriptor.
|
|
180
131
|
if (component === ANCHOR_COMPONENT)
|
|
181
132
|
recordCreateAnchor(node);
|
|
182
|
-
// A primitive whose ENTIRE subtree must vanish on this platform (
|
|
183
|
-
// Android
|
|
184
|
-
//
|
|
185
|
-
// anchor hoists its children into Fabric in its place; a void node contributes neither itself nor
|
|
186
|
-
// them.
|
|
133
|
+
// A primitive whose ENTIRE subtree must vanish on this platform (input-accessory-view on
|
|
134
|
+
// Android) resolves to the void component the same way. An anchor hoists its children into
|
|
135
|
+
// Fabric in its place; a void node contributes neither itself nor them.
|
|
187
136
|
else if (component === VOID_COMPONENT)
|
|
188
137
|
recordCreateVoid(node);
|
|
189
|
-
// `instanceHandle` is the node itself: it round-trips through Fabric unchanged and comes back
|
|
190
|
-
// the event target, and the BRAND below
|
|
138
|
+
// `instanceHandle` is the node itself: it round-trips through Fabric unchanged and comes back
|
|
139
|
+
// as the event target, and the BRAND below confirms it is one of ours.
|
|
191
140
|
else
|
|
192
141
|
recordCreateElement(node, component, isText, node);
|
|
193
142
|
// Gated on the boolean, not on the Map: this runs ~9 000 times per benchmark create, and an app
|
|
194
143
|
// that registers nothing must pay one boolean read rather than a hash lookup per node.
|
|
195
144
|
if (hasHostBehaviors())
|
|
196
145
|
attachHostBehavior(node, tag);
|
|
197
|
-
// A third-party view's own ViewConfig processors, as a fold
|
|
198
|
-
//
|
|
199
|
-
// `
|
|
200
|
-
// one component can legitimately have both.
|
|
201
|
-
//
|
|
202
|
-
// Costs a `Set.has` per node for a built-in, which is where `resolve` bails, and nothing else:
|
|
203
|
-
// the answer is cached per component name, not computed per node.
|
|
146
|
+
// A third-party view's own ViewConfig processors, as a fold, AFTER the behavior's — the behavior
|
|
147
|
+
// rewrites wrapper-body props, and `validAttributes[*].process` then converts what it produced.
|
|
148
|
+
// Costs one `Set.has` per node for a built-in, where `resolve` bails.
|
|
204
149
|
const configFold = configPayloadFold(component);
|
|
205
150
|
if (configFold !== undefined) {
|
|
206
151
|
const behaviorFold = node.payloadFold;
|
|
@@ -211,28 +156,15 @@ tag = component) {
|
|
|
211
156
|
}
|
|
212
157
|
return node;
|
|
213
158
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
*
|
|
218
|
-
* A raw text carrying a tag looks odd and is not. It has no props an app can write — its whole
|
|
219
|
-
* payload is `text` — but its CONTENT can still be a function of the platform rather than of the
|
|
220
|
-
* app: Button renders its title uppercased on Android (`Button.js:352-353`), which is a user-agent
|
|
221
|
-
* decision about a control, not anything the app asked for. That rule needs the node to be
|
|
222
|
-
* identifiable, and a tag is how this codebase identifies one.
|
|
223
|
-
*
|
|
224
|
-
* Defaulted to the raw-text component, so every existing caller is unchanged and pays the same
|
|
225
|
-
* lookup miss `createElement` already pays for a node nobody registered.
|
|
226
|
-
*/
|
|
159
|
+
// `tag` mirrors `createElement`'s: the behavior registry is keyed by tag, and a raw text's CONTENT
|
|
160
|
+
// can still be a function of the platform (Button renders its title uppercased on Android), even
|
|
161
|
+
// with no props an app can write. Defaulted to the raw-text component for existing callers.
|
|
227
162
|
export function createRawText(text, tag = RAW_TEXT_COMPONENT) {
|
|
228
163
|
const node = new SymbioteNode(RAW_TEXT_COMPONENT, false);
|
|
229
164
|
recordCreateRawText(node, text);
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
// kind is tagged. So an untagged raw text must pay a reference comparison against the default and
|
|
234
|
-
// not a registry lookup: `tag` is the same string literal in that case, so the compare is pointer
|
|
235
|
-
// equality and the intern, the op and the miss are all skipped.
|
|
165
|
+
// TAG check first, not `hasHostBehaviors()`: almost no raw text is tagged (thousands are leaves
|
|
166
|
+
// under a `<Text>`), so an untagged one pays a pointer-equality compare against the default,
|
|
167
|
+
// not a registry lookup.
|
|
236
168
|
if (tag !== RAW_TEXT_COMPONENT && hasHostBehaviors())
|
|
237
169
|
attachHostBehavior(node, tag);
|
|
238
170
|
return node;
|
|
@@ -243,11 +175,9 @@ export function createRawText(text, tag = RAW_TEXT_COMPONENT) {
|
|
|
243
175
|
export function isSymbioteNode(value) {
|
|
244
176
|
return typeof value === 'object' && value !== null && BRAND in value;
|
|
245
177
|
}
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// directly to prove whether they're the SAME node object or two different ones. Kept behind
|
|
250
|
-
// DEBUG per <keep_logs_gate_behind_DEBUG>, never removed.
|
|
178
|
+
// A WeakMap can't be logged, so this gives every node a small human-readable id, assigned lazily
|
|
179
|
+
// on first call — lets a dlog at ref-attach time and one at commit/dispatch time be compared to
|
|
180
|
+
// prove whether they're the SAME node object. Kept behind DEBUG per <keep_logs_gate_behind_DEBUG>.
|
|
251
181
|
const debugIds = new WeakMap();
|
|
252
182
|
let nextDebugId = 1;
|
|
253
183
|
export function debugNodeId(node) {
|
|
@@ -258,11 +188,9 @@ export function debugNodeId(node) {
|
|
|
258
188
|
}
|
|
259
189
|
return id;
|
|
260
190
|
}
|
|
261
|
-
// Vue's runtime-core needs comment/anchor nodes (fragments, v-if, v-for) to track
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
// (commit.ts): no native view is ever created. Marked by a sentinel component name,
|
|
265
|
-
// not a new field, so the hot SymbioteNode shape is untouched.
|
|
191
|
+
// Vue's runtime-core needs comment/anchor nodes (fragments, v-if, v-for) to track sibling order;
|
|
192
|
+
// Fabric has no such concept. An anchor is a real retained node so insert/nextSibling/parentNode
|
|
193
|
+
// ordering stays correct, but the commit walk SKIPS it — no native view is ever created.
|
|
266
194
|
export const ANCHOR_COMPONENT = '#anchor';
|
|
267
195
|
export function createAnchor() {
|
|
268
196
|
const node = new SymbioteNode(ANCHOR_COMPONENT, false);
|
|
@@ -270,40 +198,24 @@ export function createAnchor() {
|
|
|
270
198
|
return node;
|
|
271
199
|
}
|
|
272
200
|
// The sentinel a primitive resolves to when its ENTIRE subtree must vanish from Fabric on this
|
|
273
|
-
// platform
|
|
274
|
-
//
|
|
275
|
-
// children never reach Fabric either: the commit walk stops at it, recursively.
|
|
201
|
+
// platform (input-accessory-view on Android). Unlike ANCHOR_COMPONENT, whose node hoists children
|
|
202
|
+
// up, a void node's children never reach Fabric either — the commit walk stops there, recursively.
|
|
276
203
|
export const VOID_COMPONENT = '#void';
|
|
277
204
|
export function createVoid() {
|
|
278
205
|
const node = new SymbioteNode(VOID_COMPONENT, false);
|
|
279
206
|
recordCreateVoid(node);
|
|
280
207
|
return node;
|
|
281
208
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
* Angular reads `null` as "defer, `<ng-content>` will place this" (answering the surface once
|
|
287
|
-
* mounted every FlatList cell at top level), while Vue and Solid spell `?? surface` at their call
|
|
288
|
-
* sites and would be handed an object that is not the `SymbioteSurface` they compare against. One
|
|
289
|
-
* component name, read in JS, keeps all three right without a second structure.
|
|
290
|
-
*
|
|
291
|
-
* It is a JS-side name only. What goes over the wire is `RCTView`, because this node is REAL.
|
|
292
|
-
*/
|
|
209
|
+
// The sentinel a SURFACE's own root node carries, so `parentOf` can stop there. A top-level node
|
|
210
|
+
// must answer `undefined` for its parent — Angular reads `null` as "defer, ng-content will place
|
|
211
|
+
// this", while Vue/Solid spell `?? surface` and need the exact object to compare against.
|
|
212
|
+
// JS-side name only: what goes over the wire is `RCTView`, because this node is real.
|
|
293
213
|
export const SURFACE_COMPONENT = '#surface';
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
* `box-none` a touch landing outside the app's own children has no escape. Living here rather than
|
|
300
|
-
* in each adapter's `mount()` gives every framework a full-screen root for free and keeps layout in
|
|
301
|
-
* the shared layer (`<adapters_stay_thin>`).
|
|
302
|
-
*
|
|
303
|
-
* The surface therefore commits as ONE node rather than hoisting its children into the child set —
|
|
304
|
-
* which is why the host materializes the node `OP_COMMIT` names instead of walking its children. An
|
|
305
|
-
* anchor in that position still hoists, so the host handles both without a special case.
|
|
306
|
-
*/
|
|
214
|
+
// One persistent root view per surface, mirroring RN's own AppContainer: `renderApplication` wraps
|
|
215
|
+
// the app in `<View style={{flex:1}} pointerEvents="box-none">`. Not decoration — without flex:1 a
|
|
216
|
+
// non-flex root collapses to content height, and without box-none an outside touch has no escape.
|
|
217
|
+
// Commits as ONE node rather than hoisting children into the child set, so the host materializes
|
|
218
|
+
// the node OP_COMMIT names instead of walking its children (an anchor there still hoists).
|
|
307
219
|
export function createSurfaceRoot() {
|
|
308
220
|
const node = new SymbioteNode(SURFACE_COMPONENT, false);
|
|
309
221
|
recordCreateElement(node, 'RCTView', false, node);
|
|
@@ -316,101 +228,57 @@ export function createSurfaceRoot() {
|
|
|
316
228
|
export function isAnchor(node) {
|
|
317
229
|
return node.component === ANCHOR_COMPONENT;
|
|
318
230
|
}
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// Dirty-marking is GONE, along with the walk it existed to skip. An op names the node it changed,
|
|
325
|
-
// so the host marks exactly that node and its own ancestors; nothing on this side has to guess.
|
|
326
|
-
// Listener changes still record nothing, for the reason they always did: `node.listeners` never
|
|
327
|
-
// reaches Fabric, and the one listener that DOES change a Fabric prop, `layout`, raises `onLayout`
|
|
231
|
+
// A raw text with no characters must not reach Fabric — AttributedString::appendFragment drops the
|
|
232
|
+
// fragment while the text walk has already flagged "last child was raw text", so the next raw
|
|
233
|
+
// sibling merges into an empty `fragments.back()` and aborts. Enforced by the host, not here.
|
|
234
|
+
// No dirty-marking here: an op names the node it changed, so the host marks that node and its
|
|
235
|
+
// ancestors. `node.listeners` never reaches Fabric, except `layout`, which raises `onLayout`
|
|
328
236
|
// through `setProp` below.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
* engine only knows how to swap the name — the same split every other spec-driven fold has here.
|
|
335
|
-
*
|
|
336
|
-
* A no-op when the name is unchanged, so a renderer may call it on every update without comparing
|
|
337
|
-
* first.
|
|
338
|
-
*
|
|
339
|
-
* The JS field and the op BOTH move, and both are load-bearing. `node.component` is what the aria
|
|
340
|
-
* fold, the behavior registry and `fabricProps` key on; the op is what makes the host re-create the
|
|
341
|
-
* node under the new name, since no prop write moves a node between native views.
|
|
342
|
-
*/
|
|
237
|
+
// Change which Fabric view a node commits as, keeping the node's identity. Policy stays out of the
|
|
238
|
+
// engine — which prop decides, and between which views, lives in HOST_PRIMITIVES; the engine only
|
|
239
|
+
// swaps the name. A no-op when unchanged, so a renderer may call it on every update.
|
|
240
|
+
// Both the JS field and the op move: `node.component` is what the aria fold and fabricProps key
|
|
241
|
+
// on, the op is what makes the host re-create the node, since no prop write moves it between views.
|
|
343
242
|
export function setNodeComponent(node, component) {
|
|
344
243
|
if (node.component === component)
|
|
345
244
|
return;
|
|
346
245
|
node.component = component;
|
|
347
246
|
recordSetComponent(node, component);
|
|
348
247
|
}
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
// and are all GONE. Every one of them answered a question about a tree — which ancestor went stale,
|
|
352
|
-
// whether a node's PRESENCE in its parent's renderable list flipped, which anchor to climb past —
|
|
353
|
-
// and the host is the only thing that can answer those now. It marks from the ops themselves.
|
|
248
|
+
// Tree-staleness marking (which ancestor went stale, presence flips, anchor climbs) lives entirely
|
|
249
|
+
// on the host now, derived from the ops themselves — nothing here has to answer those questions.
|
|
354
250
|
// How many prop writes an adapter pushed at the engine. Read-and-zeroed through
|
|
355
|
-
// readCommitProfile() (tree-host.ts), which prices the layer ABOVE the host.
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
// keeping it here would mean reading the previous value BACK over the wire — ~44 001 reads on a
|
|
361
|
-
// 1 000-row create, exactly the crossings this design removes. So the number is no longer visible
|
|
362
|
-
// from JS, and it is not faked as a zero it would have to keep re-earning.
|
|
363
|
-
//
|
|
364
|
-
// Not gated behind isDebug(), for the same reason the commit profile is not: an integer increment
|
|
365
|
-
// is noise next to the prop write it counts, and the figure is only meaningful from a release
|
|
366
|
-
// build. A per-call dlog was the obvious alternative and is deliberately NOT here - a log line per
|
|
367
|
-
// write would measure the logging rather than the code (see the `perf-claims-need-numbers` rule).
|
|
251
|
+
// readCommitProfile() (tree-host.ts), which prices the layer ABOVE the host. No `noops` count: the
|
|
252
|
+
// `Object.is` guard lives in the host, which would need the previous value back over the wire.
|
|
253
|
+
// Not gated behind isDebug(): an integer increment is noise next to the prop write it counts, and
|
|
254
|
+
// the figure is only meaningful from a release build. No per-call dlog either — a log line per
|
|
255
|
+
// write would measure the logging rather than the code.
|
|
368
256
|
const propStats = { writes: 0 };
|
|
369
257
|
export function takePropStats() {
|
|
370
258
|
const snapshot = { writes: propStats.writes };
|
|
371
259
|
propStats.writes = 0;
|
|
372
260
|
return snapshot;
|
|
373
261
|
}
|
|
374
|
-
// `<component>.<key>` -> write count, gated behind `isDebug()
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// recordSetProp call sites directly, not just before/after counts" — by naming exactly which
|
|
378
|
-
// (view, key) pair an adapter comparison's aggregate delta is hiding, the same ledger shape F-75's
|
|
379
|
-
// payload census already uses (`RCTView.accessible 2000`).
|
|
262
|
+
// `<component>.<key>` -> write count, gated behind `isDebug()`: a Map lookup per write is real
|
|
263
|
+
// cost on the hottest path, so it only runs when someone asked. Names exactly which (view, key)
|
|
264
|
+
// pair an adapter comparison's aggregate delta is hiding.
|
|
380
265
|
let propKeyTally;
|
|
381
266
|
export function takePropKeyTally() {
|
|
382
267
|
const snapshot = propKeyTally ?? new Map();
|
|
383
268
|
propKeyTally = undefined;
|
|
384
269
|
return snapshot;
|
|
385
270
|
}
|
|
386
|
-
// A pure prop set: no event inference. `onTintColor` is a Switch prop and reaches
|
|
387
|
-
//
|
|
388
|
-
// the key
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
// to tell a removed key from one that was never there.
|
|
394
|
-
//
|
|
395
|
-
// THE `Object.is` DEDUPE IS NOT HERE ANY MORE, and that is the one behaviour change in this file.
|
|
396
|
-
// It needed the value the node already holds, and JS no longer holds it — reading it back would be
|
|
397
|
-
// ~44 001 crossings on a 1 000-row create, which is the cost this whole design exists to remove. The
|
|
398
|
-
// guard lives in the host's `OP_SET_PROP` instead, where the previous value is a local field: same
|
|
399
|
-
// comparison, same `Object.is` reasoning (a style object or a handler closure is a fresh reference
|
|
400
|
-
// on nearly every render, so it simply never fires for them, which is correct because an adapter may
|
|
401
|
-
// hand back the SAME reference with mutated contents and identity cannot see that).
|
|
271
|
+
// A pure prop set: no event inference. `onTintColor` is a Switch prop and reaches Fabric like any
|
|
272
|
+
// other; the event-vs-prop decision is made by routeProp, never by the key's name.
|
|
273
|
+
// `undefined` DELETES the key (`NO_VALUE` on the wire, mutation-buffer.ts). `null` is NOT the same:
|
|
274
|
+
// it's a legitimate Fabric value meaning "reset to default", and a merge-on-clone host must tell a
|
|
275
|
+
// removed key from one that was never there.
|
|
276
|
+
// No `Object.is` DEDUPE HERE: it needs the value the node already holds, which JS doesn't. The
|
|
277
|
+
// guard lives in the host's `OP_SET_PROP` instead, where the previous value is a local field.
|
|
402
278
|
export function setProp(node, key, value) {
|
|
403
|
-
// THE ARIA GATE WAS THIS LINE AND IT IS GONE (2026-09-18). `node.hasAriaAlias` existed to let the
|
|
404
|
-
// headless payload builder skip `foldAriaProps` on the ~99% of nodes carrying no alias; that
|
|
405
|
-
// builder no longer folds aria at all — the rule is the device's, in `SymbioteFabricProps.cpp`,
|
|
406
|
-
// which recomputes the gate from the bag it already holds. So the flag became write-only, and its
|
|
407
|
-
// write ran `isAriaAliasKey` on EVERY prop write in the engine to maintain something nothing read.
|
|
408
|
-
//
|
|
409
279
|
// A composed primitive's slot — and its wrapper, where it has one — can carry a value DERIVED
|
|
410
|
-
// from an owner prop,
|
|
411
|
-
//
|
|
412
|
-
// `setProperty` does not go through routeProp), and past the identity guard so a re-render
|
|
413
|
-
// writing an unchanged value costs them nothing. See `IHostBehavior.slotDerived`.
|
|
280
|
+
// from an owner prop, so `markPropsDirty` bubbles up or neither ever learns. Here, not in
|
|
281
|
+
// `routeProp`: this is the one choke point every writer passes. See `IHostBehavior.slotDerived`.
|
|
414
282
|
if (node.childHost !== undefined && slotDerivesFrom(node, key)) {
|
|
415
283
|
markPropsDirty(node.childHost);
|
|
416
284
|
// Past the slot: a `buildStructure` that builds a CHAIN registers the deeper nodes here, and
|
|
@@ -430,62 +298,37 @@ export function setProp(node, key, value) {
|
|
|
430
298
|
}
|
|
431
299
|
writeProp(node, key, value);
|
|
432
300
|
}
|
|
433
|
-
// Function props that never left JS, keyed by node.
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
// REGISTERED `on*` name into the listener stash before this point. Two paths get past that and both
|
|
441
|
-
// are real: an unregistered `on*` that is an ordinary prop by design (`onValueChange`, which
|
|
442
|
-
// `fabricProps` drops on the way to native and a behavior reads back), and `setNativeProps`, which
|
|
443
|
-
// bypasses `routeProp` entirely. Device-found 2026-09-08 through the second: an `Animated.View`
|
|
444
|
-
// spread with `panResponder.panHandlers` hands `AnimatedProps.__getValue()` a bag of callbacks, and
|
|
445
|
-
// it copies every key it holds.
|
|
446
|
-
//
|
|
447
|
-
// So they live here, exactly as listeners already do — and `propOf` looks here first, which is what
|
|
448
|
-
// keeps `onValueChange` readable. Nothing is lost on the native side: `fabricProps` dropped function
|
|
449
|
-
// props on both hosts anyway, so the payload is byte-identical either way.
|
|
301
|
+
// Function props that never left JS, keyed by node. A function CANNOT cross this wire:
|
|
302
|
+
// `jsi::dynamicFromValue` THROWS on a callable, killing the whole batch.
|
|
303
|
+
// Two paths reach here past `routeProp`'s registered-`on*` diversion: an unregistered `on*` that's
|
|
304
|
+
// an ordinary prop (`onValueChange`), and `setNativeProps`, which bypasses `routeProp` entirely —
|
|
305
|
+
// e.g. `Animated.View` spreading `panResponder.panHandlers` copies every key it holds.
|
|
306
|
+
// Live here like listeners do; `propOf` looks here first, keeping `onValueChange` readable.
|
|
307
|
+
// `fabricProps` drops function props on both hosts, so the payload is byte-identical either way.
|
|
450
308
|
const functionProps = new WeakMap();
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
* `setNativeProps` calls this rather than `recordSetProp` for that reason — it is the path that has
|
|
455
|
-
* no `routeProp` in front of it.
|
|
456
|
-
*/
|
|
309
|
+
// The one place a prop reaches the wire, and the only place that can keep a function off it.
|
|
310
|
+
// `setNativeProps` calls this rather than `recordSetProp` for that reason — the path with no
|
|
311
|
+
// `routeProp` in front of it.
|
|
457
312
|
export function writeProp(node, key, value) {
|
|
458
313
|
// The same strip `routeProp` does, repeated because THIS is the path with no `routeProp` in
|
|
459
|
-
// front of it
|
|
460
|
-
// `
|
|
461
|
-
// so on a JSX adapter `__self` rode straight past the strip into the host. See
|
|
462
|
-
// REACT_JSX_DEV_PROPS for what that costs on each platform.
|
|
314
|
+
// front of it: `AnimatedProps` re-sends its whole raw prop bag every frame, so on a JSX adapter
|
|
315
|
+
// `__self` rides straight past the declarative-path filter into the host.
|
|
463
316
|
if (REACT_JSX_DEV_PROPS.has(key))
|
|
464
317
|
return;
|
|
465
|
-
// Arms the node's recurring post-commit hook, for the rare node that has one. HERE
|
|
466
|
-
// `setProp
|
|
467
|
-
//
|
|
468
|
-
// the imperative write entirely (`__tests__/after-commit-lifecycle.test.ts` said so). The field
|
|
469
|
-
// read is the same shape as `hasAriaAlias` and for the same reason — this is the hottest path in
|
|
470
|
-
// the engine, and a Set lookup per write is not something it can carry.
|
|
318
|
+
// Arms the node's recurring post-commit hook, for the rare node that has one. HERE, not in
|
|
319
|
+
// `setProp`: this is where both the declarative and `setNativeProps` paths meet, so a hook armed
|
|
320
|
+
// only by the former misses the imperative write entirely.
|
|
471
321
|
if (node.hasCommitHook)
|
|
472
322
|
noteCommitHookNodeChanged(node);
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
// headless only, and the device commits the raw CSS string, which Fabric drops in silence. See
|
|
477
|
-
// `structured-style.ts`; it hands the same object back when nothing needed resolving, which is
|
|
478
|
-
// what keeps the host's identity guard and `pushClassStyle` working.
|
|
479
|
-
// Image's three source props, resolved on the way in for the same reason and at the same seam —
|
|
480
|
-
// the asset lookup is Metro's registry, which exists only in JS. See `image-source-write.ts`.
|
|
481
|
-
// Gated on the node: the resolution normalises to Image's ARRAY shape, and a `WebView` spells
|
|
482
|
-
// `source` too.
|
|
323
|
+
// `boxShadow`/`filter`/`transform` and Image's source props resolve on the way IN, at this same
|
|
324
|
+
// choke point: the C++ payload builder has no JS to do it headless. `structured-style.ts` hands
|
|
325
|
+
// back the same object when nothing needed resolving, keeping the host's identity guard intact.
|
|
483
326
|
let written = value;
|
|
484
327
|
if (key === 'style' || key === 'activeStyle') {
|
|
485
328
|
written = resolveStructuredStyle(value);
|
|
486
329
|
}
|
|
487
330
|
else if (node.resolvesImageSources && IMAGE_SOURCE_PROPS.has(key)) {
|
|
488
|
-
written = resolveImageSourceProp(value);
|
|
331
|
+
written = resolveImageSourceProp(value, key === 'source' && Platform.OS === 'android');
|
|
489
332
|
}
|
|
490
333
|
if (typeof written === 'function') {
|
|
491
334
|
let bag = functionProps.get(node);
|
|
@@ -509,24 +352,15 @@ export function writeProp(node, key, value) {
|
|
|
509
352
|
export function functionPropOf(node, key) {
|
|
510
353
|
return functionProps.get(node)?.get(key);
|
|
511
354
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
* `undefined` rather than an empty Map for a node that stashed nothing, which is nearly every node:
|
|
516
|
-
* the caller then hands back the host's own object instead of copying it.
|
|
517
|
-
*/
|
|
355
|
+
// The same stash, whole — what `propsOf` layers over the host's answer. `undefined` rather than an
|
|
356
|
+
// empty Map for a node that stashed nothing (nearly every node), so the caller hands back the
|
|
357
|
+
// host's own object instead of copying it.
|
|
518
358
|
export function functionPropsOf(node) {
|
|
519
359
|
return functionProps.get(node);
|
|
520
360
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
* A behavior whose payload is DERIVED has no prop to write: the sticky header's debounced
|
|
525
|
-
* translateY lives in its own runtime, not on the node, so nothing names the node and the host
|
|
526
|
-
* never marks it. This is the one route that says so directly.
|
|
527
|
-
*
|
|
528
|
-
* Dirtying is not publishing — pair it with `requestCommitFor` (imperative.ts).
|
|
529
|
-
*/
|
|
361
|
+
// "Rebuild this node's payload — the fold reads state I just changed." A behavior whose payload is
|
|
362
|
+
// DERIVED has no prop to write (the sticky header's debounced translateY lives in its own
|
|
363
|
+
// runtime), so this is the one route that dirties it directly. Pair with requestCommitFor.
|
|
530
364
|
export function markPropsDirty(node) {
|
|
531
365
|
flushOps();
|
|
532
366
|
// Announced to the buffer even though it writes no op: this is the one route that dirties a node
|
|
@@ -539,26 +373,14 @@ export function markPropsDirty(node) {
|
|
|
539
373
|
noteCommitHookNodeChanged(node);
|
|
540
374
|
treeHost()?.markPropsDirty(node);
|
|
541
375
|
}
|
|
542
|
-
// Fabric gates a handful of events behind a BOOLEAN prop: unlike scroll
|
|
543
|
-
// the
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
// BaseViewProps.onLayout ParagraphShadowNode.cpp / RCTViewComponentView
|
|
552
|
-
// AccessibilityProps.onAccessibilityTap RCTViewComponentView.mm:1603
|
|
553
|
-
// AccessibilityProps.onAccessibilityMagicTap RCTViewComponentView.mm:1613
|
|
554
|
-
// AccessibilityProps.onAccessibilityEscape RCTViewComponentView.mm:1623
|
|
555
|
-
// AccessibilityProps.onAccessibilityAction RCTViewComponentView.mm:1633
|
|
556
|
-
// BaseParagraphProps.onTextLayout ParagraphShadowNode.cpp:351
|
|
557
|
-
//
|
|
558
|
-
// Keyed by the post-`listenerName` event name, valued with the payload key. `magicTap` maps to
|
|
559
|
-
// `onMagicTap` and NOT to the C++ member name `onAccessibilityMagicTap`, because `onMagicTap` is
|
|
560
|
-
// what RN's own view config declares (BaseViewConfig.ios.js) - the two disagree upstream, and
|
|
561
|
-
// matching stock is the only defensible choice until RN resolves it.
|
|
376
|
+
// Fabric gates a handful of events behind a BOOLEAN prop: unlike scroll/touch/change, these fire
|
|
377
|
+
// only when the shadow node carries the flag. We drop function props from the payload, so a gated
|
|
378
|
+
// handler attaches on our side and the native event silently never arrives without this map.
|
|
379
|
+
// Exhaustive as of react-native 0.86: every `bool on*` field in Fabric's C++ props
|
|
380
|
+
// (ReactCommon/react/renderer/components/**), keyed by the post-`listenerName` event name.
|
|
381
|
+
// `magicTap` maps to `onMagicTap`, not the C++ member name `onAccessibilityMagicTap` — RN's own
|
|
382
|
+
// view config (BaseViewConfig.ios.js) disagrees with its C++ prop name, and matching stock is the
|
|
383
|
+
// only defensible choice until RN resolves it.
|
|
562
384
|
const GATED_EVENT_PROPS = new Map([
|
|
563
385
|
['layout', 'onLayout'],
|
|
564
386
|
['textLayout', 'onTextLayout'],
|
|
@@ -570,18 +392,10 @@ const GATED_EVENT_PROPS = new Map([
|
|
|
570
392
|
// The explicit event channel. Structural adapters (Svelte addEventListener, Angular
|
|
571
393
|
// Renderer2.listen) call this directly with an already-known event name; flat-bag
|
|
572
394
|
// adapters reach it through routeProp. A non-function value clears the listener.
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
* circular for the behavior's own dispatcher — it would stash itself and never occupy the slot it
|
|
578
|
-
* exists to hold. This is the one writer allowed past that gate.
|
|
579
|
-
*
|
|
580
|
-
* `undefined` removes it, gate flag included. A behavior whose dispatcher is conditional needs
|
|
581
|
-
* that as much as it needs the install: ScrollView takes the owner's `layout` only while the app
|
|
582
|
-
* or an inverted sticky header wants it, and a one-way installer leaves `onLayout: true` standing
|
|
583
|
-
* in the payload of a ScrollView that no longer reads the event.
|
|
584
|
-
*/
|
|
395
|
+
// Install a listener the BEHAVIOR owns, bypassing the ownership check: `setEventListener` diverts
|
|
396
|
+
// an owned name into the stash, which would be circular for the behavior's own dispatcher.
|
|
397
|
+
// `undefined` removes it, gate flag included — ScrollView takes the owner's `layout` only while
|
|
398
|
+
// something wants it, and a one-way installer would leave `onLayout: true` stuck in the payload.
|
|
585
399
|
export function setBehaviorListener(node, name, listener) {
|
|
586
400
|
if (listener === undefined)
|
|
587
401
|
node.listeners?.delete(name);
|
|
@@ -591,26 +405,24 @@ export function setBehaviorListener(node, name, listener) {
|
|
|
591
405
|
if (flagProp !== undefined)
|
|
592
406
|
setProp(node, flagProp, listener === undefined ? undefined : true);
|
|
593
407
|
}
|
|
408
|
+
// The unowned names whose presence a platform rule reads. See `setEventListener`.
|
|
409
|
+
const PRESSABILITY_NAMES = new Set([
|
|
410
|
+
'press',
|
|
411
|
+
'longPress',
|
|
412
|
+
'startShouldSetResponder',
|
|
413
|
+
]);
|
|
594
414
|
export function setEventListener(node, name, value) {
|
|
595
415
|
const isHandler = typeof value === 'function';
|
|
596
|
-
// A name a host behavior OWNS never reaches `node.listeners
|
|
597
|
-
// that slot and
|
|
598
|
-
//
|
|
599
|
-
// keys at stake are the ones a gesture STARTS on, so the loser is silently pressless. The
|
|
600
|
-
// component wrapper used to mediate this by destructuring the app's callbacks out before they
|
|
601
|
-
// reached the node; a tag has no mediator. Gated on the boolean first, so an app with no behavior
|
|
602
|
-
// registered pays one read.
|
|
416
|
+
// A name a host behavior OWNS never reaches `node.listeners`: the behavior's dispatcher holds
|
|
417
|
+
// that slot, and `node.listeners` is single-slot, so without this the app's callback would evict
|
|
418
|
+
// it with no diagnostic — on the keys a gesture STARTS on, silently pressless.
|
|
603
419
|
if (hasHostBehaviors() && ownsListener(node, name)) {
|
|
604
|
-
// The PRESENCE only, never the identity:
|
|
605
|
-
// hands a fresh closure nearly every render — see `markDirty`'s note on why that must stay
|
|
606
|
-
// free). A flip is a mount-time event, not a per-render one.
|
|
420
|
+
// The PRESENCE only, never the identity: a fresh closure nearly every render must not notify.
|
|
607
421
|
const wasWired = appListenerFor(node, name) !== undefined;
|
|
608
422
|
stashAppListener(node, name, isHandler ? value : undefined);
|
|
609
423
|
if (wasWired !== isHandler) {
|
|
610
|
-
// The BIT, on the flip only
|
|
611
|
-
// the app wired anything
|
|
612
|
-
// the closure ever leaving JS. Ordered before the notify so a behavior that re-commits from
|
|
613
|
-
// that callback finds the host already holding the new answer.
|
|
424
|
+
// The BIT, on the flip only, so a platform rule (`focusable` on a touchable) can resolve a
|
|
425
|
+
// key depending on whether the app wired anything, without the closure leaving JS.
|
|
614
426
|
recordSetOwnedListener(node, name, isHandler);
|
|
615
427
|
notifyOwnedListenerChange(node, name, isHandler);
|
|
616
428
|
}
|
|
@@ -619,6 +431,14 @@ export function setEventListener(node, name, value) {
|
|
|
619
431
|
setProp(node, flagged, isHandler ? true : undefined);
|
|
620
432
|
return;
|
|
621
433
|
}
|
|
434
|
+
// A plain `<text>` presses through the engine's own synthesis, with no behavior owning the
|
|
435
|
+
// names, yet its payload depends on whether it is pressable (Android `accessible`, the `link`
|
|
436
|
+
// role). Same bit as the owned path, on the flip only.
|
|
437
|
+
if (PRESSABILITY_NAMES.has(name)) {
|
|
438
|
+
const wasWired = node.listeners?.has(name) === true;
|
|
439
|
+
if (wasWired !== isHandler)
|
|
440
|
+
recordSetOwnedListener(node, name, isHandler);
|
|
441
|
+
}
|
|
622
442
|
if (isHandler) {
|
|
623
443
|
const handler = value;
|
|
624
444
|
const listeners = (node.listeners ??= new Map());
|
|
@@ -630,22 +450,17 @@ export function setEventListener(node, name, value) {
|
|
|
630
450
|
const flagProp = GATED_EVENT_PROPS.get(name);
|
|
631
451
|
if (flagProp !== undefined)
|
|
632
452
|
setProp(node, flagProp, isHandler ? true : undefined);
|
|
633
|
-
// `onLoad`/`onLoadStart`/`onLoadEnd`/`onError` are real Fabric events on `RCTImageView
|
|
634
|
-
//
|
|
635
|
-
// `
|
|
636
|
-
// from the listener map instead of from a stashed function value.
|
|
453
|
+
// `onLoad`/`onLoadStart`/`onLoadEnd`/`onError` are real Fabric events on `RCTImageView`, so they
|
|
454
|
+
// land here, not in `writeProp` — see image-source-write.ts for why Android's
|
|
455
|
+
// `shouldNotifyLoadEvents` is synthesized from the listener map, not a stashed function value.
|
|
637
456
|
if (node.resolvesImageSources && IMAGE_LOAD_EVENT_NAMES.has(name)) {
|
|
638
457
|
setProp(node, 'shouldNotifyLoadEvents', anyImageLoadEventListenerWired(node.listeners) ? true : undefined);
|
|
639
458
|
}
|
|
640
459
|
}
|
|
641
|
-
// `/^on[A-Z]/` spelled out
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
// is free: the boundary is pinned by its own tests in `node.test.ts`.
|
|
646
|
-
//
|
|
647
|
-
// 111 is 'o', 110 is 'n', and 65-90 is A-Z. `charCodeAt` past the end answers NaN, which fails every
|
|
648
|
-
// comparison — so a two-character `on` needs no length check.
|
|
460
|
+
// `/^on[A-Z]/` spelled out: this runs on EVERY prop write, and a regex costs measurably more than
|
|
461
|
+
// the character reads (mutation-api-fill-cost.itest.ts). The boundary is pinned by node.test.ts.
|
|
462
|
+
// 111 is 'o', 110 is 'n', 65-90 is A-Z. `charCodeAt` past the end answers NaN, which fails every
|
|
463
|
+
// comparison, so a two-character `on` needs no length check.
|
|
649
464
|
function isOnEventName(key) {
|
|
650
465
|
if (key.charCodeAt(0) !== 111 || key.charCodeAt(1) !== 110)
|
|
651
466
|
return false;
|
|
@@ -656,11 +471,9 @@ function isOnEventName(key) {
|
|
|
656
471
|
function listenerName(propName) {
|
|
657
472
|
return propName.charAt(2).toLowerCase() + propName.slice(3);
|
|
658
473
|
}
|
|
659
|
-
// The responder-negotiation events (PanResponder's panHandlers)
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
// any node so PanResponder's handlers actually attach (rather than routing to
|
|
663
|
-
// setProp and reaching Fabric as dead props). Names are post-listenerName.
|
|
474
|
+
// The responder-negotiation events (PanResponder's panHandlers): a JS-side protocol synthesized
|
|
475
|
+
// from raw touches, not Fabric ViewConfig events, so `isEventFor` never reports them. Treated as
|
|
476
|
+
// listeners on any node so the handlers attach instead of reaching Fabric as dead props.
|
|
664
477
|
const RESPONDER_EVENTS = new Set([
|
|
665
478
|
'startShouldSetResponder',
|
|
666
479
|
'startShouldSetResponderCapture',
|
|
@@ -675,32 +488,21 @@ const RESPONDER_EVENTS = new Set([
|
|
|
675
488
|
'responderTerminate',
|
|
676
489
|
'responderTerminationRequest',
|
|
677
490
|
]);
|
|
678
|
-
// React's JSX dev transform
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
// "WHILE IOS SILENTLY DROPS IT" IS WHAT THIS COMMENT USED TO SAY, AND IT IS WRONG. iOS converts
|
|
687
|
-
// the same value with `jsi::dynamicFromValue`, whose walk keeps no visited set, and `__self` is a
|
|
688
|
-
// module `this` — cyclic. That is not a drop, it is an endless walk inside `applyOps` that never
|
|
689
|
-
// returns and allocates as it goes: measured 2026-09-09 on examples/solid, one press on an
|
|
690
|
-
// Animated control, RAM to 15 GB and a dead JS thread. Android's loud rejection is the FRIENDLIER
|
|
691
|
-
// of the two platforms here.
|
|
692
|
-
//
|
|
693
|
-
// SFC/template authoring never produces them. Strip them here, once, so no adapter leaks React
|
|
694
|
-
// JSX dev metadata to the host, mirroring React's host config.
|
|
491
|
+
// React's JSX dev transform annotates every element with __self (the component instance) and
|
|
492
|
+
// __source. React's own Fabric host config consumes both and never forwards them; a JSX-based
|
|
493
|
+
// adapter (Vue JSX, Solid JSX) instead carries them as ordinary props, reaching Fabric.
|
|
494
|
+
// Both platforms reject it, not just Android: iOS's `jsi::dynamicFromValue` keeps no visited set,
|
|
495
|
+
// and `__self` is a cyclic module `this` — an endless walk inside `applyOps` that allocates until
|
|
496
|
+
// RAM is exhausted, worse than Android's loud `folly::dynamic` rejection.
|
|
497
|
+
// SFC/template authoring never produces them. Strip them here, once, mirroring React's host config.
|
|
695
498
|
const REACT_JSX_DEV_PROPS = new Set([
|
|
696
499
|
'__self',
|
|
697
500
|
'__source',
|
|
698
501
|
]);
|
|
699
|
-
// All slots are present from the start rather than added as they are written: one hidden class
|
|
700
|
-
// every styled node
|
|
502
|
+
// All slots are present from the start rather than added as they are written: one hidden class
|
|
503
|
+
// for every styled node, instead of a shape transition per slot.
|
|
701
504
|
// Narrowed rather than cast: `routeProp` takes `unknown`, and a bare `typeof v === 'function'`
|
|
702
|
-
// leaves TS with `Function`,
|
|
703
|
-
// actually promises.
|
|
505
|
+
// leaves TS with `Function`, callable with anything. This states the shape the contract promises.
|
|
704
506
|
function isStyleCallback(value) {
|
|
705
507
|
return typeof value === 'function';
|
|
706
508
|
}
|
|
@@ -716,52 +518,25 @@ function stylePartsOf(node) {
|
|
|
716
518
|
published: undefined,
|
|
717
519
|
});
|
|
718
520
|
}
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
// contradiction that is not one.
|
|
739
|
-
// Angular Ivy compiles every class form to per-token addClass/removeClass, and the renderer
|
|
740
|
-
// joins the accumulated tokens into ONE string before routeProp.
|
|
741
|
-
// React `className` is a string by convention.
|
|
742
|
-
// Svelte `normalizeSvelteClass` (adapters/svelte/src/class-value.ts) joins a clsx-shaped
|
|
743
|
-
// value, and hands anything else through UNCHANGED — so Svelte never sends a class
|
|
744
|
-
// MAP, but it does send a non-string class, deliberately, and it is the one live
|
|
745
|
-
// producer of the branch below.
|
|
746
|
-
//
|
|
747
|
-
// An OBJECT here is not a class map at all — `IClassNameValue` types it as an IResolvedStyle, the
|
|
748
|
-
// channel ScrollView / VirtualizedList / FlatList / ImageBackground use to hand a style through
|
|
749
|
-
// the class prop, and Svelte's `resolveSvelteClass` exists to feed it. Canonicalising that into
|
|
750
|
-
// tokens would not have been a category error only in theory: it would have hit a live producer on
|
|
751
|
-
// four components, and they would have silently lost their styling. Do not "simplify" the object
|
|
752
|
-
// branch away.
|
|
753
|
-
//
|
|
754
|
-
// What remains is an ARRAY of plain strings, which no adapter produces today and which reduces
|
|
755
|
-
// fresh on every call, so it gets neither a pressed variant nor `isAlreadyPublished`. Narrow, and
|
|
756
|
-
// closable by joining an all-string array before the string path — not done here.
|
|
757
|
-
//
|
|
758
|
-
// The identity reasoning underneath: the registry memoises a class STRING to the same object, and
|
|
759
|
-
// `isAlreadyPublished` compares slot 0 with Object.is. A variant built from a value that resolves
|
|
760
|
-
// fresh each call could never be turned away by the guard, and 1 000 unpressed rows would
|
|
761
|
-
// republish and re-dirty — the storm the guard exists to stop.
|
|
762
|
-
// Slot 1's twin of `baseStyleOf`. The variant stands in for the AUTHORED style, so it replaces
|
|
763
|
-
// slot 1 and not slot 0 — it must beat the class cascade exactly the way the authored style does,
|
|
764
|
-
// and a `:active` class rule must still be able to win slot 0 underneath it.
|
|
521
|
+
// The pressed variant is a complete REPLACEMENT, not an overlay: `resolveActiveClassName` resolves
|
|
522
|
+
// the element's tokens PLUS `:active` through the same matcher, so the result already contains
|
|
523
|
+
// everything the base class gave. No extra slot needed; the published array's shape stays put.
|
|
524
|
+
// Resolved LAZILY, at press time, never beside `classStyle` — eager would double the resolutions
|
|
525
|
+
// on every class WRITE to serve a state almost no node is ever in. A press is one event on one
|
|
526
|
+
// node, so the extra lookup there is invisible.
|
|
527
|
+
// `:active` applies only to a class reaching the engine as a STRING — true for nearly every
|
|
528
|
+
// producer: Vue/Angular/React all normalize `class` to a string before it reaches here, except
|
|
529
|
+
// Svelte's `normalizeSvelteClass`, the one live producer of a non-string class below.
|
|
530
|
+
// An OBJECT here is not a class map — `IClassNameValue` types it as an IResolvedStyle, the channel
|
|
531
|
+
// ScrollView/VirtualizedList/FlatList/ImageBackground use to hand a style through the class prop.
|
|
532
|
+
// Canonicalising it into tokens would silently break their styling — do not "simplify" it away.
|
|
533
|
+
// An ARRAY of plain strings: no adapter produces this today, and it reduces fresh every call, so
|
|
534
|
+
// it gets neither a pressed variant nor `isAlreadyPublished`.
|
|
535
|
+
// The registry memoises a class STRING to the same object; `isAlreadyPublished` compares slot 0
|
|
536
|
+
// with Object.is. A variant resolving fresh each call could never be turned away by that guard, so
|
|
537
|
+
// unpressed rows would republish and re-dirty forever — the storm the guard exists to stop.
|
|
538
|
+
// Slot 1's twin of `baseStyleOf`: the variant replaces slot 1, not slot 0, so it beats the class
|
|
539
|
+
// cascade the way the authored style does, while a `:active` rule can still win slot 0 underneath.
|
|
765
540
|
function explicitStyleOf(parts) {
|
|
766
541
|
return parts.isPressed && parts.activeStyle !== undefined
|
|
767
542
|
? parts.activeStyle
|
|
@@ -772,55 +547,30 @@ function baseStyleOf(parts) {
|
|
|
772
547
|
? resolveActiveClassName(parts.className)
|
|
773
548
|
: parts.classStyle;
|
|
774
549
|
}
|
|
775
|
-
// Republish the merged style after one half changed.
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
// get skipped by setProp's Object.is guard, and never restore the declarative style the animation
|
|
786
|
-
// overwrote. The re-push IS the restore path.
|
|
787
|
-
// Would `pushClassStyle` republish an array byte-identical to the one already standing?
|
|
788
|
-
//
|
|
789
|
-
// Sound because `pushClassStyle` is the ONLY writer of `parts.published` — both routeProp branches
|
|
790
|
-
// and setNodeHidden funnel through it — so a node that has published nothing holds `undefined` and
|
|
791
|
-
// the first write can never be swallowed.
|
|
792
|
-
// What a node publishes when NOTHING resolves — an unstyled node, or the benchmark row's
|
|
793
|
-
// `style={isSelected ? {…} : undefined}` on the 999 rows that are not selected. Length 0 is the
|
|
794
|
-
// marker and needs no second field: `pushClassStyle` never publishes an empty array otherwise, so
|
|
795
|
-
// the state is unambiguous, and it is distinct from `undefined`, which means "nothing published
|
|
796
|
-
// yet" and must never be turned away.
|
|
550
|
+
// Republish the merged style after one half changed. Halves are written IN PLACE by the callers
|
|
551
|
+
// below — no patch object, no spread — because this is the hottest function in the mutation API.
|
|
552
|
+
// The fresh ARRAY allocation stays, DELIBERATELY: `setNativeProps` bypasses these parts, so an app
|
|
553
|
+
// handing over a hoisted style constant would get skipped by the Object.is guard and never
|
|
554
|
+
// restore the declarative style an animation overwrote. The re-push IS the restore path.
|
|
555
|
+
// Sound because `pushClassStyle` is the ONLY writer of `parts.published` — so a node that has
|
|
556
|
+
// published nothing holds `undefined`, and the first write can never be swallowed as "unchanged".
|
|
557
|
+
// What a node publishes when NOTHING resolves. Length 0 is the marker, needing no second field:
|
|
558
|
+
// `pushClassStyle` never otherwise publishes an empty array, and it's distinct from `undefined`
|
|
559
|
+
// ("nothing published yet", which must never be turned away).
|
|
797
560
|
const PUBLISHED_NOTHING = Object.freeze([]);
|
|
798
561
|
// ── ONE ARRAY PER DISTINCT PAIR, SHARED ACROSS NODES ─────────────────────────────────────────────
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
//
|
|
807
|
-
// `WeakMap`, and both levels of it, so nothing here can grow without bound: a caller that builds a
|
|
808
|
-
// fresh style object per render gets a fresh cache entry that dies with the object. That caller also
|
|
809
|
-
// gets no sharing, which is correct — two structurally equal objects are two values to whoever reads
|
|
810
|
-
// them, and a deep compare would make every prop write cost the size of the style.
|
|
811
|
-
//
|
|
812
|
-
// The three-slot (hidden) form is deliberately NOT cached. `display: 'none'` is a state almost no
|
|
813
|
-
// node is ever in, so a third map would be paid for on every write to serve a case that is rare by
|
|
814
|
-
// construction.
|
|
562
|
+
// `mutation-buffer.ts` interns values BY IDENTITY, so distinct-but-equal arrays across a list
|
|
563
|
+
// styled the same way cost distinct entries and separate JS -> `folly::dynamic` conversions.
|
|
564
|
+
// `WeakMap`, both levels, so nothing grows unbounded: a fresh style object per render gets a fresh
|
|
565
|
+
// cache entry that dies with the object, and correctly gets no sharing — two structurally equal
|
|
566
|
+
// objects are two values to whoever reads them.
|
|
567
|
+
// The three-slot (hidden) form is deliberately NOT cached: `display: 'none'` is rare by
|
|
568
|
+
// construction, so a third map would be paid for on every write to serve it.
|
|
815
569
|
const sharedPairByExplicit = new WeakMap();
|
|
816
570
|
const sharedPairByBase = new WeakMap();
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
* `undefined` when the pair cannot be keyed (a primitive half, or the hidden form), and the caller
|
|
821
|
-
* then builds its own array exactly as before. Sharing is an optimization here, never a requirement:
|
|
822
|
-
* every reader of `published` compares its SLOTS by identity, never the array itself.
|
|
823
|
-
*/
|
|
571
|
+
// The published array for this pair — same object every time the same two parts are handed in.
|
|
572
|
+
// `undefined` when the pair can't be keyed (a primitive half, or the hidden form), and the caller
|
|
573
|
+
// then builds its own array; every reader compares slots by identity, never the array itself.
|
|
824
574
|
function sharedStylePair(base, explicit) {
|
|
825
575
|
const baseIsKeyable = typeof base === 'object' && base !== null;
|
|
826
576
|
const explicitIsKeyable = typeof explicit === 'object' && explicit !== null;
|
|
@@ -864,49 +614,28 @@ function sharedStylePair(base, explicit) {
|
|
|
864
614
|
return made;
|
|
865
615
|
}
|
|
866
616
|
// A slot that contributes no keys to the payload: absent, or the registry's shared "this class
|
|
867
|
-
// styles nothing" object. An IDENTITY compare
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
/** A plain style bag — not an array of styles, not a callback, not null. */
|
|
617
|
+
// styles nothing" object. An IDENTITY compare, not a key count — `Object.keys(x).length` allocates
|
|
618
|
+
// an array, and this runs on every class and style write.
|
|
619
|
+
// A plain style bag — not an array of styles, not a callback, not null.
|
|
871
620
|
function isStyleRecord(value) {
|
|
872
621
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
873
622
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
* that can see it is a no-op.
|
|
884
|
-
*
|
|
885
|
-
* SHALLOW AND CONSERVATIVE, both deliberately. A nested value (a transform list, a shadow, a style
|
|
886
|
-
* array) reports "not the same" rather than being compared deeply, because a deep compare makes this
|
|
887
|
-
* guard cost the size of the style — which is the cost it exists to avoid. Those keep crossing and
|
|
888
|
-
* the host's own `diffProps` refuses them exactly as before, so being wrong here is slow, never
|
|
889
|
-
* incorrect.
|
|
890
|
-
*
|
|
891
|
-
* `undefined` on either side also reports "not the same", which is what lets the key COUNT stand in
|
|
892
|
-
* for a key-set comparison: equal counts plus every key of `next` matching a defined value in
|
|
893
|
-
* `standing` cannot leave a key unaccounted for.
|
|
894
|
-
*/
|
|
623
|
+
// Is this rebuilt style the same style, key for key? A component body writing its style inline
|
|
624
|
+
// hands over a fresh object every render, equal to the one already standing, which Object.is
|
|
625
|
+
// cannot see — without this the write crosses into the host and is only found unchanged there.
|
|
626
|
+
// Shallow and conservative, deliberately: a nested value (transform list, shadow, style array)
|
|
627
|
+
// reports "not the same" rather than being deep-compared, so being wrong here is slow, never
|
|
628
|
+
// incorrect — the host's own diffProps still refuses those exactly as before.
|
|
629
|
+
// `undefined` on either side also reports "not the same", which lets the key COUNT stand in for a
|
|
630
|
+
// key-set comparison: equal counts plus every key of `next` matching a defined value in `standing`
|
|
631
|
+
// cannot leave a key unaccounted for.
|
|
895
632
|
export function isSameShallowStyle(next, standing) {
|
|
896
|
-
// THE SAME OBJECT IS THE SAME STYLE,
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
// IT CHANGES NOTHING OBSERVABLE, and that was break-tested rather than assumed. Inverting it — an
|
|
904
|
-
// identical object reporting "changed" — leaves all 1 044 unit tests and all 512 itests green,
|
|
905
|
-
// because `pushClassStyle`'s own `isAlreadyPublished` catches the republish downstream:
|
|
906
|
-
// `sharedStylePair` memoizes the pair by the explicit object, so the array it rebuilds is
|
|
907
|
-
// identity-equal to the one standing. The saving is the two key arrays and the walk, and nothing
|
|
908
|
-
// else. A speed change with no behaviour signature has to be justified by a measurement alone
|
|
909
|
-
// (method §8), which is why the number above is in this comment rather than in a commit message.
|
|
633
|
+
// THE SAME OBJECT IS THE SAME STYLE, checked first: without it, a re-push of a hoisted constant
|
|
634
|
+
// (what Solid does on every signal change, having no diff) allocates two key arrays and walks
|
|
635
|
+
// them to reach the same answer the identity check gives for free.
|
|
636
|
+
// Changes nothing OBSERVABLE — break-tested, not assumed: inverting this line (identical object
|
|
637
|
+
// reporting "changed") leaves the full test suite green, because `pushClassStyle`'s own
|
|
638
|
+
// `isAlreadyPublished` catches the republish downstream via `sharedStylePair`'s memoization.
|
|
910
639
|
if (next === standing)
|
|
911
640
|
return isStyleRecord(next);
|
|
912
641
|
if (!isStyleRecord(next) || !isStyleRecord(standing))
|
|
@@ -957,46 +686,21 @@ function isAlreadyPublished(parts) {
|
|
|
957
686
|
: published.length === 3 && Object.is(published[2], parts.hiddenStyle);
|
|
958
687
|
}
|
|
959
688
|
function pushClassStyle(node, parts) {
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
// effect re-runs whenever any signal it reads changes, so a list-wide signal makes every row
|
|
964
|
-
// re-push its own unchanged class. Measured on device 2026-08-23 (examples/solid, once its
|
|
965
|
-
// primitives were tags): selecting one row of 1 000 read WRITES 1001 and a 10.3 ms reconcile
|
|
966
|
-
// window against Fabric's unmoved 0/0/10 — a thousand-node dirty walk for two nodes of change.
|
|
967
|
-
//
|
|
968
|
-
// This is NOT the naive skip the paragraph above forbids, and the published marker is the
|
|
969
|
-
// difference. Skipping on "the parts are unchanged" alone would break the restore path, because
|
|
970
|
-
// setNativeProps writes the style slot past this function and a hoisted style constant would then
|
|
971
|
-
// never be restored. But setNativeProps CLEARS `parts.published` — so after any bypass
|
|
972
|
-
// isAlreadyPublished is false, the re-push happens exactly as before, and the restore path is
|
|
973
|
-
// untouched.
|
|
974
|
-
//
|
|
975
|
-
// Exact rather than approximate: resolveClassName memoizes a class STRING to the same object, so
|
|
976
|
-
// an unchanged class yields an identity-equal classStyle. It deliberately does not fire for an
|
|
977
|
-
// object/array class value, which resolves fresh every call — the same place the host's own
|
|
978
|
-
// Object.is guard gives up on a style object, so no new asymmetry appears.
|
|
689
|
+
// An unchanged class still reaches this write — Solid has no diff, so a list-wide signal
|
|
690
|
+
// re-pushes every row's class and dirties the whole tree. Guard keys off `published`, which
|
|
691
|
+
// setNativeProps clears, so an imperative restore still re-publishes correctly.
|
|
979
692
|
if (isAlreadyPublished(parts))
|
|
980
693
|
return;
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
// array, store, and re-compare on every later commit. Both are behaviourally "no style": the
|
|
985
|
-
// payload builder flattens the pair of undefineds into no keys at all.
|
|
986
|
-
//
|
|
987
|
-
// This is NOT the naive skip the note above forbids, and the distinction is the same one the
|
|
988
|
-
// published marker makes. A restore after `setNativeProps` arrives here with `published` cleared
|
|
989
|
-
// to `undefined`, so it is never turned away — and when the authored style is nothing, restoring
|
|
990
|
-
// it means DELETING the slot the imperative write put there, which is what this emits.
|
|
694
|
+
// Emits NO_VALUE rather than `[undefined, undefined]` — the host skips a real array with one
|
|
695
|
+
// pointer check instead of building and diffing a `folly::dynamic`. Same published-marker guard
|
|
696
|
+
// as above keeps the restore path working after setNativeProps clears it.
|
|
991
697
|
if (hasNothingToPublish(parts)) {
|
|
992
698
|
parts.published = PUBLISHED_NOTHING;
|
|
993
699
|
setProp(node, 'style', undefined);
|
|
994
700
|
return;
|
|
995
701
|
}
|
|
996
|
-
//
|
|
997
|
-
//
|
|
998
|
-
// in — and this project spent a day removing per-frame allocations, so a slot that is undefined
|
|
999
|
-
// 99.9% of the time does not get to ride along on every style write.
|
|
702
|
+
// Third slot only appended while hidden — a permanent 3-element array would add an allocation
|
|
703
|
+
// to every style write for a state most nodes never enter.
|
|
1000
704
|
const base = baseStyleOf(parts);
|
|
1001
705
|
const explicit = explicitStyleOf(parts);
|
|
1002
706
|
const published = parts.hiddenStyle === undefined
|
|
@@ -1008,81 +712,49 @@ function pushClassStyle(node, parts) {
|
|
|
1008
712
|
// `display: 'none'` is a real RN style value (Yoga's DisplayNone), so a hidden node keeps its
|
|
1009
713
|
// place in the tree, its state and its children — it just stops laying out and painting.
|
|
1010
714
|
const HIDDEN_STYLE = { display: 'none' };
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
* The seam React's `Activity`/`Suspense` reach for through `hideInstance`/`unhideInstance`. It
|
|
1015
|
-
* lives in the engine rather than an adapter because the reversibility problem — restoring the
|
|
1016
|
-
* author's style byte for byte — belongs to whoever owns the style merge, and that is here.
|
|
1017
|
-
*/
|
|
715
|
+
// Stop a node painting without unmounting it, or let it paint again — the seam React's
|
|
716
|
+
// Activity/Suspense reach for through hideInstance/unhideInstance. Lives in the engine, not an
|
|
717
|
+
// adapter, since restoring the author's style byte belongs to whoever owns the style merge.
|
|
1018
718
|
export function setNodeHidden(node, hidden) {
|
|
1019
719
|
const parts = stylePartsOf(node);
|
|
1020
720
|
parts.hiddenStyle = hidden ? HIDDEN_STYLE : undefined;
|
|
1021
721
|
pushClassStyle(node, parts);
|
|
1022
722
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
* than a component (`.claude/rules/host-primitive-tier.md`, tier 2). A component is forced only
|
|
1029
|
-
* when the TEMPLATE must read the state — `v-slot="{ pressed }"` and the function form of `style`
|
|
1030
|
-
* — and this exists so the common case does not have to.
|
|
1031
|
-
*
|
|
1032
|
-
* Costs nothing when no `:active` rule is registered anywhere: `resolveActiveClassName` hands back
|
|
1033
|
-
* the very same object the unpressed path returns, so `isAlreadyPublished` turns the re-push away
|
|
1034
|
-
* and the node is never dirtied.
|
|
1035
|
-
*/
|
|
723
|
+
// Put a node into (or out of) its pressed state, so `:active` rules apply. The engine-owned half:
|
|
724
|
+
// press state resolves below the framework, which is what lets a pressable stay an intrinsic tag
|
|
725
|
+
// instead of a component — a component is forced only when the template must read the state.
|
|
726
|
+
// Costs nothing when no `:active` rule is registered: resolveActiveClassName hands back the same
|
|
727
|
+
// object the unpressed path returns, so isAlreadyPublished turns the re-push away.
|
|
1036
728
|
export function setNodePressed(node, pressed) {
|
|
1037
729
|
const parts = stylePartsOf(node);
|
|
1038
730
|
parts.isPressed = pressed;
|
|
1039
731
|
pushClassStyle(node, parts);
|
|
1040
732
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
* drives a rule that lives in C++ (`foldTouchableHighlightUnderlay`), so it crosses as one op rather
|
|
1047
|
-
* than resolving to a style here. And the two are genuinely different facts: RN holds the underlay
|
|
1048
|
-
* past release so a fast tap still flashes, so `shown` LAGS `pressed` by a `delayPressOut` timer.
|
|
1049
|
-
*
|
|
1050
|
-
* No style is computed on this side at all, which is the whole point — the two props the rule reads
|
|
1051
|
-
* (`underlayColor`, `activeOpacity`) are ones the engine already strips from the payload, so the
|
|
1052
|
-
* values and their defaults live in one place instead of being erased in C++ and reached around for
|
|
1053
|
-
* in JS.
|
|
1054
|
-
*/
|
|
733
|
+
// Tell the host a behavior's FEEDBACK is showing — TouchableHighlight's underlay, and only that.
|
|
734
|
+
// Deliberately not the same bit as setNodePressed: this drives a rule living in C++
|
|
735
|
+
// (foldTouchableHighlightUnderlay), crossing as one op, since `shown` lags `pressed` by a timer.
|
|
736
|
+
// No style computed here at all: the two props the rule reads (underlayColor, activeOpacity) are
|
|
737
|
+
// ones the engine already strips from the payload, so their defaults live in one place.
|
|
1055
738
|
export function setNodeUnderlayShown(node, shown) {
|
|
1056
739
|
recordSetUnderlayShown(node, shown);
|
|
1057
740
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
* The one caller is `setNativeProps` (imperative.ts), which writes the style slot past this file —
|
|
1062
|
-
* see the note on `IClassStyleParts.published` for why the restore path depends on this. A no-op for
|
|
1063
|
-
* a node nobody has styled, which is why it is not `stylePartsOf(node).published = undefined`: that
|
|
1064
|
-
* would allocate the parts on a node that has none.
|
|
1065
|
-
*/
|
|
741
|
+
// Forget what was last published, so the next pushClassStyle cannot be turned away. The one
|
|
742
|
+
// caller is setNativeProps, which writes the style slot past this file. A no-op for a node nobody
|
|
743
|
+
// has styled — that's why this isn't `stylePartsOf(node).published = undefined`.
|
|
1066
744
|
export function clearPublishedStyle(node) {
|
|
1067
745
|
if (node.styleParts !== undefined)
|
|
1068
746
|
node.styleParts.published = undefined;
|
|
1069
747
|
}
|
|
1070
|
-
// The explicit (non-class
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
1073
|
-
// [classStyle, explicitStyle] array commitClassStyle writes above.
|
|
748
|
+
// The explicit (non-class) style half — an adapter that builds style key-by-key (Angular's
|
|
749
|
+
// ɵɵstyleProp/setStyle) merges onto this, not node.props.style directly, which may hold the
|
|
750
|
+
// [classStyle, explicitStyle] pair pushClassStyle publishes.
|
|
1074
751
|
export function getExplicitStyle(node) {
|
|
1075
752
|
return node.styleParts?.explicitStyle;
|
|
1076
753
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
* For a caller that wants the merged answer without a host: the pair reaches the payload as an op,
|
|
1082
|
-
* and only a host holds ops. A test that has not installed one — `core/css-parser` reaches into the
|
|
1083
|
-
* engine by relative path and depends on neither package — can read it here instead of reaching
|
|
1084
|
-
* into `styleParts`, which is engine-owned and not a shape anything outside may bind to.
|
|
1085
|
-
*/
|
|
754
|
+
// The `[classStyle, explicitStyle]` pair the node currently publishes, same order pushClassStyle
|
|
755
|
+
// writes, so flattenStyle collapses it the way Fabric will.
|
|
756
|
+
// For a caller that wants the merged answer without a host: the pair reaches the payload as an op,
|
|
757
|
+
// and only a host holds ops — core/css-parser reads it here instead of reaching into styleParts.
|
|
1086
758
|
export function getPublishedStyle(node) {
|
|
1087
759
|
const parts = node.styleParts;
|
|
1088
760
|
if (parts === undefined)
|
|
@@ -1090,44 +762,23 @@ export function getPublishedStyle(node) {
|
|
|
1090
762
|
return [parts.classStyle, parts.explicitStyle];
|
|
1091
763
|
}
|
|
1092
764
|
const CLASS_PROP_KEYS = new Set(['class', 'className']);
|
|
1093
|
-
//
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
// `id
|
|
1098
|
-
//
|
|
1099
|
-
// Fabric drops it in SILENCE: a rename that half-works loses the nativeID with nothing red anywhere.
|
|
765
|
+
// Flat-bag split (React/Vue/Solid): `onX` becomes a listener only when the component's ViewConfig
|
|
766
|
+
// declares `x` as an event — otherwise it's a plain prop, so `onTintColor` on a Switch (whose only
|
|
767
|
+
// event is `change`) routes to setProp and reaches Fabric untouched.
|
|
768
|
+
// `id` is RN's alias for `nativeID` and wins when both are set (View.js: `nativeID = id`). No
|
|
769
|
+
// ViewConfig declares raw `id`, so Fabric drops it — a half-working rename loses nativeID with
|
|
770
|
+
// nothing red anywhere.
|
|
1100
771
|
const ID_ALIAS_FROM = 'id';
|
|
1101
772
|
const ID_ALIAS_TO = 'nativeID';
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
*
|
|
1112
|
-
* HERE because this is the funnel: every adapter's prop write ends at `routeProp`, whatever shape it
|
|
1113
|
-
* starts in. A bag fold cannot serve the per-key renderers and a per-key fold cannot serve the bag
|
|
1114
|
-
* ones; the seam they SHARE can serve both.
|
|
1115
|
-
*
|
|
1116
|
-
* PRECEDENCE IS WHY THIS NEEDS STATE. Upstream reads both props at once, so `id ?? nativeID` is
|
|
1117
|
-
* decided in one expression. A per-key writer never sees both, so precedence would otherwise fall
|
|
1118
|
-
* out of write ORDER — `<view id nativeID>` keeping the stale legacy value while `<view nativeID id>`
|
|
1119
|
-
* did not. Solid had already built exactly this memory for exactly this reason; it is one copy now.
|
|
1120
|
-
*
|
|
1121
|
-
* The authored `nativeID` is REMEMBERED rather than discarded, so clearing the `id` hands the slot
|
|
1122
|
-
* back instead of latching. A framework that unsets a prop between renders must get the other
|
|
1123
|
-
* source back.
|
|
1124
|
-
*
|
|
1125
|
-
* PRECEDENCE ITSELF IS PER-COMPONENT. `View.js`'s `id ?? nativeID` is the default (`idWins` below),
|
|
1126
|
-
* but `TouchableWithoutFeedback.js`'s clone composes that and then runs a `PASSTHROUGH_PROPS` loop
|
|
1127
|
-
* that unconditionally overwrites `nativeID` with the raw authored value when set (`:279-282`) — an
|
|
1128
|
-
* app authoring both ends up with `nativeID` winning there. `node.nativeIdWinsOverId`, set by
|
|
1129
|
-
* `attachHostBehavior` for the one behavior that declares it, flips which side wins.
|
|
1130
|
-
*/
|
|
773
|
+
// The ONE place this rename happens: HERE because every adapter's prop write ends at routeProp,
|
|
774
|
+
// whatever shape it starts in — a bag fold can't serve the per-key renderers and a per-key fold
|
|
775
|
+
// can't serve the bag ones, but the seam they share can serve both.
|
|
776
|
+
// Precedence needs state because upstream decides `id ?? nativeID` in one expression; a per-key
|
|
777
|
+
// writer never sees both, so unmemoized precedence would fall out of write order instead. The
|
|
778
|
+
// authored nativeID is remembered, so clearing `id` hands the slot back rather than latching.
|
|
779
|
+
// Precedence is per-component: View.js's `id ?? nativeID` is the default, but
|
|
780
|
+
// TouchableWithoutFeedback's clone unconditionally overwrites nativeID with the authored value
|
|
781
|
+
// when set. `node.nativeIdWinsOverId`, set for the one behavior that declares it, flips the winner.
|
|
1131
782
|
const idAliased = new WeakMap();
|
|
1132
783
|
function routeIdAlias(node, key, value) {
|
|
1133
784
|
const state = idAliased.get(node) ?? {
|
|
@@ -1147,46 +798,34 @@ function routeIdAlias(node, key, value) {
|
|
|
1147
798
|
export function routeProp(node, key, value) {
|
|
1148
799
|
if (REACT_JSX_DEV_PROPS.has(key))
|
|
1149
800
|
return;
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
// registry. The redirected write recurses into the slot's own `routeProp`, which is single-hop
|
|
1157
|
-
// by construction: a slot has no slot of its own (`childHost` is documented single-hop, and
|
|
1158
|
-
// `buildStructure` is what would have to nest one).
|
|
801
|
+
// Prop twin of the child redirect in `appendChild` — a composed primitive's owner receives props
|
|
802
|
+
// that belong to its internal slot (`contentContainerStyle` on ScrollView styles the content
|
|
803
|
+
// view), same reason the owner is named for a child: that's where the app wrote it.
|
|
804
|
+
// Gated on the field, so a node with no slot pays one load+branch and never touches the registry.
|
|
805
|
+
// The redirect recurses into the slot's own routeProp, single-hop by construction — a slot has no
|
|
806
|
+
// slot of its own (`childHost` is documented single-hop).
|
|
1159
807
|
if (node.childHost !== undefined) {
|
|
1160
808
|
const slotKey = slotPropNameFor(node, key);
|
|
1161
809
|
if (slotKey !== undefined) {
|
|
1162
|
-
// A class
|
|
1163
|
-
// `
|
|
1164
|
-
//
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
// only ever show on the tag path.
|
|
1168
|
-
routeProp(node.childHost, slotKey === 'style' && typeof value === 'string' ? 'class' : slotKey, value);
|
|
810
|
+
// A class name is a legal spelling of `contentContainerStyle`, so a string must land on the
|
|
811
|
+
// slot as `class`, not `style` — renamed verbatim it would publish a style holding a string,
|
|
812
|
+
// dropped with nothing red. React's wrapper resolves the name itself; this only matters here.
|
|
813
|
+
const slotValueFor = node.hostBehavior?.slotValueFor;
|
|
814
|
+
routeProp(node.childHost, slotKey === 'style' && typeof value === 'string' ? 'class' : slotKey, slotValueFor === undefined ? value : slotValueFor(slotKey, value));
|
|
1169
815
|
return;
|
|
1170
816
|
}
|
|
1171
817
|
}
|
|
1172
|
-
//
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1175
|
-
// redirect sends it to. Resolved before the redirect, the alias would publish a `nativeID` on the
|
|
1176
|
-
// wrapper and the inner node would never see it: the owner would answer to a testID the app
|
|
1177
|
-
// pointed at the image.
|
|
818
|
+
// After the slot redirect on purpose: a composed primitive forwards most of its bag to an
|
|
819
|
+
// internal node (ImageBackground spreads everything but `style` onto its image), so an `id` on
|
|
820
|
+
// the owner belongs there. Resolved earlier, nativeID would land on the wrapper unseen.
|
|
1178
821
|
if (key === ID_ALIAS_FROM || key === ID_ALIAS_TO) {
|
|
1179
822
|
routeIdAlias(node, key, value);
|
|
1180
823
|
return;
|
|
1181
824
|
}
|
|
1182
|
-
// An AnimatedNode
|
|
1183
|
-
//
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
// unchanged. See `animated/host-binding.ts`; the gate is one boolean for an app that animates
|
|
1187
|
-
// nothing.
|
|
1188
|
-
//
|
|
1189
|
-
// AFTER the slot redirect, so an animated `contentContainerStyle` binds on the node that
|
|
825
|
+
// An AnimatedNode in a prop (`style={{opacity: value}}`) resolves here to the value to publish,
|
|
826
|
+
// the engine holding the subscription. Returns its input by identity when nothing is animated,
|
|
827
|
+
// so every branch below is unchanged (animated/host-binding.ts).
|
|
828
|
+
// After the slot redirect, so an animated `contentContainerStyle` binds on the node that
|
|
1190
829
|
// actually carries the style.
|
|
1191
830
|
const resolved = hasAnimatedNodes()
|
|
1192
831
|
? bindAnimatedValue(node, key, value)
|
|
@@ -1203,32 +842,26 @@ export function routeProp(node, key, value) {
|
|
|
1203
842
|
}
|
|
1204
843
|
if (key === 'style') {
|
|
1205
844
|
const parts = stylePartsOf(node);
|
|
1206
|
-
// A
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1209
|
-
//
|
|
1210
|
-
//
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
//
|
|
1215
|
-
// The callback must be PURE in `pressed`: its result is read once per state, here and under
|
|
1216
|
-
// every transform's emission (`core/components/src/state-style.ts` carries the same contract).
|
|
845
|
+
// A function `style` (`style={({pressed}) => …}`) arrives here intact and is resolved at both
|
|
846
|
+
// states — writing `style` + `activeStyle` as an explicit pair, cheaper by one call per
|
|
847
|
+
// recompute than the app doing it by hand.
|
|
848
|
+
// Without this the failure is silent: a function isn't an `on*` name, misses setEventListener,
|
|
849
|
+
// lands in setProp as a function value, and fabricProps drops function props — the node commits
|
|
850
|
+
// with no style at all.
|
|
851
|
+
// The callback must be pure in `pressed` — read once per state, here and under every
|
|
852
|
+
// transform's emission (core/components/src/state-style.ts carries the same contract).
|
|
1217
853
|
if (isStyleCallback(resolved)) {
|
|
1218
854
|
parts.explicitStyle = resolved({ pressed: false });
|
|
1219
855
|
parts.activeStyle = resolved({ pressed: true });
|
|
1220
856
|
parts.activeStyleFromCallback = true;
|
|
1221
857
|
}
|
|
1222
858
|
else {
|
|
1223
|
-
// A
|
|
1224
|
-
//
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
//
|
|
1230
|
-
// Gated on the previous write NOT having come from a callback, because that one owns
|
|
1231
|
-
// `parts.activeStyle` and the branch below has to clear it. Returning early would leave the old
|
|
859
|
+
// A rebuilt literal equal to what is standing is not a change — see `isSameShallowStyle`.
|
|
860
|
+
// Gated on something being published, which keeps the restore path intact: a setNativeProps
|
|
861
|
+
// write clears `parts.published`, and after that this must never turn a write away — the
|
|
862
|
+
// re-push IS the restore, same mechanism isAlreadyPublished relies on.
|
|
863
|
+
// Gated on the previous write not coming from a callback, since that one owns
|
|
864
|
+
// `parts.activeStyle` and this branch must clear it — returning early would leave the old
|
|
1232
865
|
// pressed look standing under a plain style.
|
|
1233
866
|
if (parts.published !== undefined &&
|
|
1234
867
|
!parts.activeStyleFromCallback &&
|
|
@@ -1252,37 +885,23 @@ export function routeProp(node, key, value) {
|
|
|
1252
885
|
if (key === 'activeStyle') {
|
|
1253
886
|
const parts = stylePartsOf(node);
|
|
1254
887
|
parts.activeStyle = resolved;
|
|
1255
|
-
// Slot 1 is no longer ours
|
|
1256
|
-
//
|
|
1257
|
-
//
|
|
1258
|
-
// engine never derived. An author writes either a callback or an explicit pair, never both for
|
|
1259
|
-
// one node — but a flat-bag adapter routes a bag key by key and can deliver that sequence.
|
|
888
|
+
// Slot 1 is no longer ours — whatever a callback derived has just been replaced. Without this
|
|
889
|
+
// the flag outlives its value: this branch overwrites the slot silently and a later plain
|
|
890
|
+
// `style` clears a variant the engine never derived, a sequence a flat-bag adapter can deliver.
|
|
1260
891
|
parts.activeStyleFromCallback = false;
|
|
1261
892
|
pushClassStyle(node, parts);
|
|
1262
893
|
return;
|
|
1263
894
|
}
|
|
1264
|
-
// RN's snapshot affordance (
|
|
1265
|
-
// pressed with no gesture.
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
// the wrong seam.
|
|
1275
|
-
//
|
|
1276
|
-
// TouchableHighlight's half of the same prop is NOT here: it PAINTS an underlay, so it is a rule
|
|
1277
|
-
// in `SymbioteFabricProps.cpp`. Two mechanisms, one prop name, because that is what upstream has.
|
|
1278
|
-
// A SIDE EFFECT AND A PASSTHROUGH, not a consume, and the difference is load-bearing in both
|
|
1279
|
-
// directions. The pressed state is set here; the prop ALSO goes on to `node.props`, because
|
|
1280
|
-
// TouchableHighlight's rule reads it off the authored bag to paint its underlay
|
|
1281
|
-
// (`foldTouchableHighlightUnderlay`). Returning early — the first spelling — left that rule blind
|
|
1282
|
-
// and turned three of its cases red. Keeping it out of the PAYLOAD is a separate job and already
|
|
1283
|
-
// done, by `kPressableMachineKeys` in `SymbioteFabricProps.cpp`.
|
|
1284
|
-
//
|
|
1285
|
-
// The same shape `GATED_EVENT_PROPS` uses above: act, then let the write continue.
|
|
895
|
+
// RN's snapshot affordance (Pressable.js seeds usePressState with it): render the control
|
|
896
|
+
// pressed with no gesture. Selects activeStyle and any `:active` class — exactly what isPressed
|
|
897
|
+
// already decides, so it belongs beside activeStyle rather than in a behavior.
|
|
898
|
+
// Here rather than in attachAfterCommit: a behavior hook reading this prop costs a post-commit
|
|
899
|
+
// crossing per pressable node (budgeted in crossing-and-payload-census.probe.test.tsx). This
|
|
900
|
+
// branch is one string compare on the first commit — no crossing at all.
|
|
901
|
+
// TouchableHighlight's half of the same prop paints an underlay, so it's a separate rule in
|
|
902
|
+
// SymbioteFabricProps.cpp — a side effect AND a passthrough. Returning early here left that rule
|
|
903
|
+
// blind; keeping it out of the payload is kPressableMachineKeys's job, done separately.
|
|
904
|
+
// Same shape GATED_EVENT_PROPS uses above: act, then let the write continue.
|
|
1286
905
|
if (key === 'testOnly_pressed')
|
|
1287
906
|
setNodePressed(node, resolved === true);
|
|
1288
907
|
if (isOnEventName(key)) {
|
|
@@ -1292,11 +911,9 @@ export function routeProp(node, key, value) {
|
|
|
1292
911
|
bindAnimatedEvent(node, key, resolved);
|
|
1293
912
|
const name = listenerName(key);
|
|
1294
913
|
const isRegisteredEvent = RESPONDER_EVENTS.has(name) || isEventFor(node.component, name);
|
|
1295
|
-
//
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1298
|
-
// indistinguishable from "the button did nothing" at the UI. Scoped to RNS* to avoid noise
|
|
1299
|
-
// from the rest of the app. Kept behind DEBUG per <keep_logs_gate_behind_DEBUG>, never removed.
|
|
914
|
+
// RNS* views derive events from react-native-screens' own codegen ViewConfig, so an
|
|
915
|
+
// unregistered event falls through to setProp as a dead prop Fabric ignores — indistinguishable
|
|
916
|
+
// from "the button did nothing" at the UI. Scoped to RNS* to avoid noise; gated behind DEBUG.
|
|
1300
917
|
if (node.component.startsWith('RNS')) {
|
|
1301
918
|
dlog(`routeProp: ${node.component} "${key}" -> listener "${name}" ` +
|
|
1302
919
|
`registered=${isRegisteredEvent} at t=${Date.now()}`);
|
|
@@ -1308,27 +925,19 @@ export function routeProp(node, key, value) {
|
|
|
1308
925
|
}
|
|
1309
926
|
setProp(node, key, resolved);
|
|
1310
927
|
}
|
|
1311
|
-
// Counted in
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1315
|
-
// standing text would mean reading it back from the host. The host holds it as a local field and
|
|
1316
|
-
// dedupes there. Two consequences it also absorbs, both of which used to be spelled here — a write
|
|
1317
|
-
// to or from `''` takes this node out of its parent's renderable child list or puts it back, and a
|
|
1318
|
-
// raw text REPARENTED under a `<Text>` commits as RCTVirtualText instead of RCTText.
|
|
928
|
+
// Counted in propStats — a text write IS a prop write, reaching Fabric as RCTRawText's only prop.
|
|
929
|
+
// Unguarded, same reason setProp is: comparing against the standing text means reading it back
|
|
930
|
+
// from the host, which holds it locally and dedupes there — including empty-string child-list
|
|
931
|
+
// transitions and reparenting under `<Text>` (RCTVirtualText vs RCTText).
|
|
1319
932
|
export function setText(node, text) {
|
|
1320
933
|
propStats.writes += 1;
|
|
1321
934
|
recordSetText(node, text);
|
|
1322
935
|
}
|
|
1323
|
-
//
|
|
1324
|
-
// parent it currently has before linking it,
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1329
|
-
// What they DO still decide in JS is which node an op names, and there are two such redirects — a
|
|
1330
|
-
// composed primitive's slot and a wrap claim. Both are read off a field on the node, so a tree with
|
|
1331
|
-
// neither pays one load and one branch per op.
|
|
936
|
+
// Structural ops: each is one op and nothing else — the host detaches a child from whatever
|
|
937
|
+
// parent it currently has before linking it, true even when an adapter names a stale one (a MOVE
|
|
938
|
+
// spells as remove-then-insert). JS doesn't track the old parent and doesn't need to.
|
|
939
|
+
// What JS still decides is which node an op names — two redirects, a composed primitive's slot and
|
|
940
|
+
// a wrap claim, both read off a field so a plain node pays one load and one branch per op.
|
|
1332
941
|
// The host's raw answer, surface INCLUDED — unlike `parentOf` (host-access.ts), which reports a
|
|
1333
942
|
// top-level node as parentless by design. The two swaps below have to NAME the holder in an op, and
|
|
1334
943
|
// for a wrapped node sitting directly under a surface that holder is the surface node.
|
|
@@ -1340,15 +949,12 @@ function holderOf(node) {
|
|
|
1340
949
|
// Which node a child actually lands on. See `ISymbioteNode.childHost`: the adapter always names the
|
|
1341
950
|
// OWNER, and a node whose behavior built an internal subtree redirects the app's children into it —
|
|
1342
951
|
// unless the behavior CLAIMS this particular child, which keeps it on the owner (`claimedChildren`).
|
|
1343
|
-
//
|
|
1344
952
|
// SINGLE HOP, not a loop, and the field's own comment says why — a chain would put a walk on the
|
|
1345
953
|
// engine's hottest path to express a depth no primitive has. A behavior needing depth points
|
|
1346
954
|
// `childHost` at the innermost node itself.
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
//
|
|
1350
|
-
// which would be a second read to save nothing. The claim check sits BEHIND that branch, so only a
|
|
1351
|
-
// slot-bearing node ever pays the registry probe.
|
|
955
|
+
// Reads a field undefined on every node with no composed primitive — one load, one branch.
|
|
956
|
+
// Deliberately not behind hasHostBehaviors() (a second read for nothing); the claim check sits
|
|
957
|
+
// behind that branch, so only a slot-bearing node pays the registry probe.
|
|
1352
958
|
function hostFor(parent, child) {
|
|
1353
959
|
const slot = parent.childHost;
|
|
1354
960
|
if (slot === undefined)
|
|
@@ -1359,16 +965,13 @@ function hostFor(parent, child) {
|
|
|
1359
965
|
return parent;
|
|
1360
966
|
return claimModeFor(parent, child.component) === undefined ? slot : parent;
|
|
1361
967
|
}
|
|
1362
|
-
// The node a child must be inserted
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
// A SIBLING slot is the opposite placement: RN paints the background image first and the app's
|
|
1370
|
-
// children over it (ImageBackground.js:80-102), so they append past it rather than in front of it —
|
|
1371
|
-
// which is what `undefined` here leaves alone.
|
|
968
|
+
// The node a child must be inserted before, or `undefined` for an ordinary append.
|
|
969
|
+
// A host that still has a slot is an owner taking a claimed child, which goes before the slot
|
|
970
|
+
// whatever the framework asked — RN renders `{refreshControl}{content}` in that order, and
|
|
971
|
+
// `beforeChild` lives inside the slot anyway.
|
|
972
|
+
// A sibling slot is the opposite: RN paints the background image first and children over it
|
|
973
|
+
// (ImageBackground.js), so they append past it rather than in front — what `undefined` leaves
|
|
974
|
+
// alone.
|
|
1372
975
|
function slotAnchorOf(host) {
|
|
1373
976
|
const slot = host.childHost;
|
|
1374
977
|
if (slot === undefined || !slotTakesChildren(host))
|
|
@@ -1376,21 +979,12 @@ function slotAnchorOf(host) {
|
|
|
1376
979
|
return slot;
|
|
1377
980
|
}
|
|
1378
981
|
// ── the two structural recorders, and why nothing here calls the raw ones ───────────────────────
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
//
|
|
1385
|
-
/**
|
|
1386
|
-
* Arm a parent's recurring post-commit hook for a STRUCTURAL change.
|
|
1387
|
-
*
|
|
1388
|
-
* A prop write is not the only thing a behavior can be waiting for, and the ScrollView sticky-header
|
|
1389
|
-
* machine is the case that proves it: it drops a wrapper when the framework takes the wrapped child
|
|
1390
|
-
* away, which writes no prop on the wrapper's owner at all. Narrowing the beat to prop writes alone
|
|
1391
|
-
* left it holding a wrapper around nothing, and its own test said so — the third behavior needing
|
|
1392
|
-
* the beat, and the only one whose source comment does not say why.
|
|
1393
|
-
*/
|
|
982
|
+
// `mayHaveChildren` is only sound if every op that gives a node a child raises it — raised here
|
|
983
|
+
// rather than at each of the five call sites, since a forgotten one would make childrenOf answer
|
|
984
|
+
// "empty" for a node that has children: a wrong answer, not a slow one.
|
|
985
|
+
// Arm a parent's recurring post-commit hook for a STRUCTURAL change, not just a prop write: the
|
|
986
|
+
// ScrollView sticky-header machine drops a wrapper when the framework takes the wrapped child
|
|
987
|
+
// away, writing no prop on the wrapper's owner at all — narrowing the beat to props left it stuck.
|
|
1394
988
|
function armCommitHookForChildChange(parent) {
|
|
1395
989
|
if (parent.hasCommitHook)
|
|
1396
990
|
noteCommitHookNodeChanged(parent);
|
|
@@ -1413,11 +1007,9 @@ function placedNode(node) {
|
|
|
1413
1007
|
}
|
|
1414
1008
|
// Make `child` the owner's parent, in place. Returns false when this is not a wrap claim, so the
|
|
1415
1009
|
// two inserts fall through to the ordinary path on one call.
|
|
1416
|
-
//
|
|
1417
|
-
//
|
|
1418
|
-
//
|
|
1419
|
-
// owner has no holder and only the second op runs. The later `appendChild(root, owner)` then
|
|
1420
|
-
// inserts the wrapper instead, because `placedNode` says so.
|
|
1010
|
+
// The owner being unattached is the normal case: every adapter fills a node's children before
|
|
1011
|
+
// appending it to its own parent, so the wrap usually happens with no holder yet and only the
|
|
1012
|
+
// second op runs — the later appendChild(root, owner) inserts the wrapper via placedNode.
|
|
1421
1013
|
function wrapsOwner(owner, child) {
|
|
1422
1014
|
if (owner.childHost === undefined)
|
|
1423
1015
|
return false;
|
|
@@ -1473,11 +1065,9 @@ export function appendChild(requestedParent, child) {
|
|
|
1473
1065
|
if (hasHostBehaviors())
|
|
1474
1066
|
notifyChildInserted(parent, placed);
|
|
1475
1067
|
}
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
// is -1, and the insert fell through to a push. On the wire it cannot: a slot has to name a node, so
|
|
1480
|
-
// an unanchored insert IS an append and is recorded as one.
|
|
1068
|
+
// No anchor means append, and it's a real case, not a defensive guard: solid-js/universal spells
|
|
1069
|
+
// "insert at end" as `insertNode(parent, node, null)`, and Vue passes `anchor` through as `null`.
|
|
1070
|
+
// A slot has to name a node, so an unanchored insert IS an append and is recorded as one.
|
|
1481
1071
|
export function insertBefore(requestedParent, child, beforeChild) {
|
|
1482
1072
|
if (wrapsOwner(requestedParent, child))
|
|
1483
1073
|
return;
|
|
@@ -1509,20 +1099,17 @@ export function removeChild(requestedParent, child) {
|
|
|
1509
1099
|
markDetachCandidate(child);
|
|
1510
1100
|
return;
|
|
1511
1101
|
}
|
|
1512
|
-
// A slot that IS the child being removed stops being one. Only a behavior that adopts an
|
|
1513
|
-
// child as its slot
|
|
1514
|
-
//
|
|
1515
|
-
// node being removed, and the child stays committed under a parent the framework believes it
|
|
1516
|
-
// left — and the NEXT child appended nests inside the orphan.
|
|
1102
|
+
// A slot that IS the child being removed stops being one. Only a behavior that adopts an app
|
|
1103
|
+
// child as its slot reaches this; without the clear, hostFor below redirects the removal into
|
|
1104
|
+
// the node being removed, and the next child appended nests inside the orphan.
|
|
1517
1105
|
if (requestedParent.childHost === child)
|
|
1518
1106
|
requestedParent.childHost = undefined;
|
|
1519
1107
|
// Redirected for the same reason the two inserts are: the adapter removes from the node it
|
|
1520
1108
|
// appended to, which is the OWNER, while the child actually lives in the slot.
|
|
1521
1109
|
const parent = hostFor(requestedParent, child);
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
// whole teardown — and none of it can matter before a behavior has actually attached to something.
|
|
1110
|
+
// hasAttachedBehaviors, not hasHostBehaviors: the latter is on from module load in every app just
|
|
1111
|
+
// from registering Pressable as a type. Nominating a candidate crosses every removed node into JS
|
|
1112
|
+
// on the commit sweep, which can't matter before a behavior has actually attached to anything.
|
|
1526
1113
|
if (hasAttachedBehaviors() || hasAnimatedBindings())
|
|
1527
1114
|
markDetachCandidate(child);
|
|
1528
1115
|
// BOTH, and the owner is the one that matters: a composed primitive's behavior lives on the node
|
|
@@ -1532,16 +1119,9 @@ export function removeChild(requestedParent, child) {
|
|
|
1532
1119
|
armCommitHookForChildChange(parent);
|
|
1533
1120
|
recordRemoveChild(parent, placedNode(child));
|
|
1534
1121
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
*
|
|
1539
|
-
* It walks nothing here: the walk needs `props.text` to tell an empty raw text from a real one, and
|
|
1540
|
-
* a child list to measure a flatten width, and JS has neither. `undefined` from `treeHost()` means
|
|
1541
|
-
* nothing is installed, and the empty census is the honest answer — every probe that reads this
|
|
1542
|
-
* asserts against a mounted tree, so a zero from an uninstalled host cannot be mistaken for one from
|
|
1543
|
-
* an empty one.
|
|
1544
|
-
*/
|
|
1122
|
+
// A structural census of the tree the HOST holds — see ITreeCensus (tree-host.ts). Walks nothing
|
|
1123
|
+
// here: the walk needs props.text and a child list, which JS has neither. `undefined` from
|
|
1124
|
+
// treeHost() means nothing installed, so the empty census can't be mistaken for a real zero.
|
|
1545
1125
|
export function censusRetainedTree(roots) {
|
|
1546
1126
|
flushOps();
|
|
1547
1127
|
return treeHost()?.census(roots) ?? EMPTY_CENSUS;
|