@symbiote-native/engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/build/accessibility-info/index.android.d.ts +3 -0
- package/build/accessibility-info/index.android.js +166 -0
- package/build/accessibility-info/index.d.ts +1 -0
- package/build/accessibility-info/index.ios.d.ts +3 -0
- package/build/accessibility-info/index.ios.js +219 -0
- package/build/accessibility-info/index.js +5 -0
- package/build/accessibility-info/shared.d.ts +34 -0
- package/build/accessibility-info/shared.js +13 -0
- package/build/action-sheet-ios/index.d.ts +36 -0
- package/build/action-sheet-ios/index.js +74 -0
- package/build/alert/index.android.d.ts +5 -0
- package/build/alert/index.android.js +117 -0
- package/build/alert/index.d.ts +1 -0
- package/build/alert/index.ios.d.ts +7 -0
- package/build/alert/index.ios.js +83 -0
- package/build/alert/index.js +8 -0
- package/build/alert/shared.d.ts +19 -0
- package/build/alert/shared.js +17 -0
- package/build/animated/animated-component-shared.d.ts +5 -0
- package/build/animated/animated-component-shared.js +54 -0
- package/build/animated/animation.d.ts +9 -0
- package/build/animated/animation.js +6 -0
- package/build/animated/animations/base.d.ts +27 -0
- package/build/animated/animations/base.js +90 -0
- package/build/animated/animations/composition.d.ts +38 -0
- package/build/animated/animations/composition.js +236 -0
- package/build/animated/animations/decay.d.ts +22 -0
- package/build/animated/animations/decay.js +65 -0
- package/build/animated/animations/raf.d.ts +5 -0
- package/build/animated/animations/raf.js +39 -0
- package/build/animated/animations/spring-config.d.ts +6 -0
- package/build/animated/animations/spring-config.js +55 -0
- package/build/animated/animations/spring.d.ts +50 -0
- package/build/animated/animations/spring.js +207 -0
- package/build/animated/animations/timing.d.ts +27 -0
- package/build/animated/animations/timing.js +101 -0
- package/build/animated/animations/tracking.d.ts +14 -0
- package/build/animated/animations/tracking.js +43 -0
- package/build/animated/bezier.d.ts +1 -0
- package/build/animated/bezier.js +101 -0
- package/build/animated/color.d.ts +37 -0
- package/build/animated/color.js +183 -0
- package/build/animated/easing.d.ts +20 -0
- package/build/animated/easing.js +96 -0
- package/build/animated/event.d.ts +36 -0
- package/build/animated/event.js +252 -0
- package/build/animated/graph.d.ts +38 -0
- package/build/animated/graph.js +227 -0
- package/build/animated/index.d.ts +20 -0
- package/build/animated/index.js +28 -0
- package/build/animated/interpolation-node.d.ts +16 -0
- package/build/animated/interpolation-node.js +57 -0
- package/build/animated/interpolation.d.ts +22 -0
- package/build/animated/interpolation.js +199 -0
- package/build/animated/mock.d.ts +56 -0
- package/build/animated/mock.js +127 -0
- package/build/animated/native/native-animated.d.ts +43 -0
- package/build/animated/native/native-animated.js +146 -0
- package/build/animated/operators.d.ts +80 -0
- package/build/animated/operators.js +266 -0
- package/build/animated/props.d.ts +20 -0
- package/build/animated/props.js +187 -0
- package/build/animated/style.d.ts +26 -0
- package/build/animated/style.js +187 -0
- package/build/animated/value-xy.d.ts +35 -0
- package/build/animated/value-xy.js +106 -0
- package/build/animated/value.d.ts +36 -0
- package/build/animated/value.js +185 -0
- package/build/app-registry/index.d.ts +40 -0
- package/build/app-registry/index.js +144 -0
- package/build/app-state/index.d.ts +16 -0
- package/build/app-state/index.js +105 -0
- package/build/appearance/index.d.ts +12 -0
- package/build/appearance/index.js +84 -0
- package/build/back-handler/index.d.ts +14 -0
- package/build/back-handler/index.js +106 -0
- package/build/commit.d.ts +16 -0
- package/build/commit.js +678 -0
- package/build/debug.d.ts +5 -0
- package/build/debug.js +18 -0
- package/build/dimensions/index.d.ts +28 -0
- package/build/dimensions/index.js +148 -0
- package/build/dispatch.d.ts +2 -0
- package/build/dispatch.js +18 -0
- package/build/events/index.d.ts +1 -0
- package/build/events/index.js +691 -0
- package/build/fabric.d.ts +32 -0
- package/build/fabric.js +59 -0
- package/build/host-instance/index.d.ts +11 -0
- package/build/host-instance/index.js +49 -0
- package/build/i18n-manager/index.d.ts +13 -0
- package/build/i18n-manager/index.js +91 -0
- package/build/index.d.ts +80 -0
- package/build/index.js +72 -0
- package/build/interaction-manager/index.d.ts +45 -0
- package/build/interaction-manager/index.js +222 -0
- package/build/keyboard/index.d.ts +31 -0
- package/build/keyboard/index.js +142 -0
- package/build/layout-animation/index.d.ts +66 -0
- package/build/layout-animation/index.js +183 -0
- package/build/linking/index.android.d.ts +2 -0
- package/build/linking/index.android.js +18 -0
- package/build/linking/index.d.ts +1 -0
- package/build/linking/index.ios.d.ts +2 -0
- package/build/linking/index.ios.js +9 -0
- package/build/linking/index.js +6 -0
- package/build/linking/shared.d.ts +32 -0
- package/build/linking/shared.js +98 -0
- package/build/native-events.d.ts +24 -0
- package/build/native-events.js +129 -0
- package/build/native-modules.d.ts +6 -0
- package/build/native-modules.js +57 -0
- package/build/node.d.ts +36 -0
- package/build/node.js +194 -0
- package/build/pan-responder/index.d.ts +53 -0
- package/build/pan-responder/index.js +353 -0
- package/build/permissions-android/index.d.ts +115 -0
- package/build/permissions-android/index.js +185 -0
- package/build/pixel-ratio/index.d.ts +8 -0
- package/build/pixel-ratio/index.js +27 -0
- package/build/platform/index.android.d.ts +22 -0
- package/build/platform/index.android.js +60 -0
- package/build/platform/index.d.ts +1 -0
- package/build/platform/index.ios.d.ts +18 -0
- package/build/platform/index.ios.js +62 -0
- package/build/platform/index.js +5 -0
- package/build/platform/shared.d.ts +25 -0
- package/build/platform/shared.js +41 -0
- package/build/platform-color.d.ts +19 -0
- package/build/platform-color.js +25 -0
- package/build/post-commit.d.ts +4 -0
- package/build/post-commit.js +16 -0
- package/build/process-aspect-ratio.d.ts +1 -0
- package/build/process-aspect-ratio.js +34 -0
- package/build/process-background-image/index.d.ts +28 -0
- package/build/process-background-image/index.js +557 -0
- package/build/process-box-shadow/index.d.ts +11 -0
- package/build/process-box-shadow/index.js +193 -0
- package/build/process-filter.d.ts +31 -0
- package/build/process-filter.js +304 -0
- package/build/process-font-variant.d.ts +1 -0
- package/build/process-font-variant.js +17 -0
- package/build/process-transform/index.d.ts +5 -0
- package/build/process-transform/index.js +120 -0
- package/build/process-transform-origin/index.d.ts +3 -0
- package/build/process-transform-origin/index.js +108 -0
- package/build/registry.d.ts +31 -0
- package/build/registry.js +145 -0
- package/build/settings/index.d.ts +8 -0
- package/build/settings/index.js +126 -0
- package/build/share/index.android.d.ts +3 -0
- package/build/share/index.android.js +56 -0
- package/build/share/index.d.ts +1 -0
- package/build/share/index.ios.d.ts +3 -0
- package/build/share/index.ios.js +47 -0
- package/build/share/index.js +6 -0
- package/build/share/shared.d.ts +32 -0
- package/build/share/shared.js +32 -0
- package/build/status-bar/index.android.d.ts +5 -0
- package/build/status-bar/index.android.js +83 -0
- package/build/status-bar/index.d.ts +1 -0
- package/build/status-bar/index.ios.d.ts +5 -0
- package/build/status-bar/index.ios.js +66 -0
- package/build/status-bar/index.js +4 -0
- package/build/status-bar/shared.d.ts +22 -0
- package/build/status-bar/shared.js +22 -0
- package/build/style/index.d.ts +1 -0
- package/build/style/index.js +30 -0
- package/build/style-registry/index.d.ts +11 -0
- package/build/style-registry/index.js +165 -0
- package/build/style-sheet/index.d.ts +20 -0
- package/build/style-sheet/index.js +121 -0
- package/build/styles.d.ts +220 -0
- package/build/styles.js +7 -0
- package/build/surface.d.ts +16 -0
- package/build/surface.js +67 -0
- package/build/tags.d.ts +1 -0
- package/build/tags.js +10 -0
- package/build/text-input-state.d.ts +5 -0
- package/build/text-input-state.js +29 -0
- package/build/toast-android/index.d.ts +10 -0
- package/build/toast-android/index.js +108 -0
- package/build/vibration/index.android.d.ts +2 -0
- package/build/vibration/index.android.js +18 -0
- package/build/vibration/index.d.ts +1 -0
- package/build/vibration/index.ios.d.ts +2 -0
- package/build/vibration/index.ios.js +54 -0
- package/build/vibration/index.js +6 -0
- package/build/vibration/shared.d.ts +15 -0
- package/build/vibration/shared.js +68 -0
- package/build/view-config.d.ts +1 -0
- package/build/view-config.js +114 -0
- package/package.json +41 -0
package/build/commit.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
// The clone-on-write engine. Fabric is persistent: you never mutate a committed
|
|
2
|
+
// node, you clone it with new props/children and atomically hand a fresh child
|
|
3
|
+
// set to completeRoot.
|
|
4
|
+
//
|
|
5
|
+
// Incremental strategy: each retained node keeps a "mirror" of what Fabric
|
|
6
|
+
// currently holds for it: its handle, the flat props last sent, the child
|
|
7
|
+
// identities last committed, and the resolved view name. On commit we walk the
|
|
8
|
+
// retained tree and only clone the nodes that actually changed; an untouched
|
|
9
|
+
// sibling subtree is reused by reference. That both skips work and preserves the
|
|
10
|
+
// native view state (scroll offset, text cursor) that a full rebuild would wipe
|
|
11
|
+
// on every commit. A change bubbles up: re-cloning a leaf
|
|
12
|
+
// forces each ancestor to re-clone too, because a persistent parent holds
|
|
13
|
+
// references to specific child handles. That bubble is inherent to a persistent
|
|
14
|
+
// tree and is exactly what React's own Fabric renderer does.
|
|
15
|
+
import { getSlot, } from './fabric';
|
|
16
|
+
import { createElement, isAnchor, RAW_TEXT_COMPONENT, VIRTUAL_TEXT_COMPONENT, } from './node';
|
|
17
|
+
import { dlog, isDebug } from './debug';
|
|
18
|
+
import { flattenStyle } from './style';
|
|
19
|
+
import { registeredProcessor } from './registry';
|
|
20
|
+
import { nextTag } from './tags';
|
|
21
|
+
import { isOpaqueColorValue } from './platform-color';
|
|
22
|
+
import { processBoxShadow } from './process-box-shadow';
|
|
23
|
+
import { registerPostCommit, runPostCommitHooks } from './post-commit';
|
|
24
|
+
import { processFilter } from './process-filter';
|
|
25
|
+
import { processTransformOrigin } from './process-transform-origin';
|
|
26
|
+
import { processTransform } from './process-transform';
|
|
27
|
+
import { processAspectRatio } from './process-aspect-ratio';
|
|
28
|
+
import { processFontVariant } from './process-font-variant';
|
|
29
|
+
import { processBackgroundImage } from './process-background-image';
|
|
30
|
+
// Per-commit work counters, surfaced via dlog so a device run can prove the
|
|
31
|
+
// engine is incremental (created=0 with clones after the first mount).
|
|
32
|
+
const stats = { created: 0, cloneProps: 0, cloneChildren: 0, reused: 0 };
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null;
|
|
35
|
+
}
|
|
36
|
+
// Diagnostic (gated): Fabric serializes props to folly::dynamic, which rejects a JS
|
|
37
|
+
// Symbol or function with "JS Symbols are not convertible to dynamic". A hard native
|
|
38
|
+
// throw deep in cloneNode*. Walk a props payload and return the dotted path of the
|
|
39
|
+
// first non-serializable leaf (Symbol / function), or undefined when clean, so the
|
|
40
|
+
// offending key is named in logcat instead of a bare stack at the JSI boundary.
|
|
41
|
+
//
|
|
42
|
+
// Bounded on purpose: a real Fabric prop tree is shallow (style/transform ~depth 3) and
|
|
43
|
+
// a leaked React element trips at depth 2 (`children` -> element -> $$typeof). `seen`
|
|
44
|
+
// breaks reference cycles and DEPTH caps runaway nesting, so the diagnostic itself can
|
|
45
|
+
// never overflow the stack on cyclic props (an event-carrying handler, a self-referential
|
|
46
|
+
// style). A crashing guard would be worse than the bug it hunts.
|
|
47
|
+
const NON_SERIALIZABLE_SCAN_DEPTH = 6;
|
|
48
|
+
function firstNonSerializablePath(value, path, depth, seen) {
|
|
49
|
+
const kind = typeof value;
|
|
50
|
+
if (kind === 'symbol' || kind === 'function')
|
|
51
|
+
return `${path}=<${kind}>`;
|
|
52
|
+
if (depth >= NON_SERIALIZABLE_SCAN_DEPTH)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
if (seen.has(value))
|
|
56
|
+
return undefined;
|
|
57
|
+
seen.add(value);
|
|
58
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
59
|
+
const found = firstNonSerializablePath(value[index], `${path}[${index}]`, depth + 1, seen);
|
|
60
|
+
if (found !== undefined)
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (isRecord(value)) {
|
|
66
|
+
if (seen.has(value))
|
|
67
|
+
return undefined;
|
|
68
|
+
seen.add(value);
|
|
69
|
+
for (const key of Object.keys(value)) {
|
|
70
|
+
const next = path === '' ? key : `${path}.${key}`;
|
|
71
|
+
const found = firstNonSerializablePath(value[key], next, depth + 1, seen);
|
|
72
|
+
if (found !== undefined)
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
// Name the offending prop before the JSI boundary throws. Gated, so the deep walk only
|
|
79
|
+
// runs while debugging; in production the clone proceeds straight to native.
|
|
80
|
+
function guardSerializable(propsDiff, viewName, tag) {
|
|
81
|
+
if (!isDebug())
|
|
82
|
+
return;
|
|
83
|
+
const bad = firstNonSerializablePath(propsDiff, '', 0, new WeakSet());
|
|
84
|
+
if (bad !== undefined)
|
|
85
|
+
dlog(`NON-SERIALIZABLE prop on ${viewName}#${tag}: ${bad}`);
|
|
86
|
+
}
|
|
87
|
+
// Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++
|
|
88
|
+
// color parser silently drops strings. The actual conversion (processColor) is
|
|
89
|
+
// RN-platform-specific, so it is injected here rather than imported, keeping
|
|
90
|
+
// shared free of a react-native dependency (and the headless harness working).
|
|
91
|
+
const COLOR_PROPS = new Set([
|
|
92
|
+
'backgroundColor',
|
|
93
|
+
'color',
|
|
94
|
+
'borderColor',
|
|
95
|
+
'borderTopColor',
|
|
96
|
+
'borderRightColor',
|
|
97
|
+
'borderBottomColor',
|
|
98
|
+
'borderLeftColor',
|
|
99
|
+
// Logical (writing-direction-relative) border colors + the block axis, all wired to
|
|
100
|
+
// processColor in RN's ReactNativeStyleAttributes. borderStartColor/borderEndColor are
|
|
101
|
+
// even publicly typed ColorValue, so they silently dropped on iOS / threw on Android.
|
|
102
|
+
'borderStartColor',
|
|
103
|
+
'borderEndColor',
|
|
104
|
+
'borderBlockColor',
|
|
105
|
+
'borderBlockStartColor',
|
|
106
|
+
'borderBlockEndColor',
|
|
107
|
+
'shadowColor',
|
|
108
|
+
// Text shadow + the W3C `outline`/image `overlay` colors, also processColor in RN.
|
|
109
|
+
'textShadowColor',
|
|
110
|
+
'overlayColor',
|
|
111
|
+
'outlineColor',
|
|
112
|
+
'tintColor',
|
|
113
|
+
// TextInput color props. iOS's native input accepts a CSS string, but Android's
|
|
114
|
+
// AndroidTextInput is strict ("ColorValue: the value must be a number or Object"),
|
|
115
|
+
// so these must be processColor'd here too, same as any other color reaching Fabric.
|
|
116
|
+
'placeholderTextColor',
|
|
117
|
+
'selectionColor',
|
|
118
|
+
'cursorColor',
|
|
119
|
+
'underlineColorAndroid',
|
|
120
|
+
// Text decoration color (underline/strike): same Fabric strictness as any color.
|
|
121
|
+
'textDecorationColor',
|
|
122
|
+
'selectionHandleColor',
|
|
123
|
+
// Switch track/thumb colors. RN processColors each via the Switch ViewConfig
|
|
124
|
+
// (SwitchNativeComponent / AndroidSwitchNativeComponent validAttributes). iOS takes
|
|
125
|
+
// onTintColor (ON) / tintColor (OFF); Android takes trackColorForTrue/False +
|
|
126
|
+
// trackTintColor, and Android's ColorPropConverter is strict ("the value must be a
|
|
127
|
+
// number or Object"), so a raw CSS string crashes. thumbTintColor reaches both.
|
|
128
|
+
'onTintColor',
|
|
129
|
+
'thumbTintColor',
|
|
130
|
+
'trackColorForTrue',
|
|
131
|
+
'trackColorForFalse',
|
|
132
|
+
'trackTintColor',
|
|
133
|
+
]);
|
|
134
|
+
// Accepts a CSS string or an opaque PlatformColor / DynamicColorIOS object. RN's
|
|
135
|
+
// processColor (the value the canary injects) handles both, resolving the opaque
|
|
136
|
+
// shapes to the platform ints/dicts iOS UIColor expects.
|
|
137
|
+
let colorProcessor = value => value;
|
|
138
|
+
export function setColorProcessor(process) {
|
|
139
|
+
colorProcessor = process;
|
|
140
|
+
}
|
|
141
|
+
// Public mirror of RN's processColor: run a color through the injected platform
|
|
142
|
+
// processor (the canary wires RN's own). Off a real host it resolves CSS strings
|
|
143
|
+
// and opaque PlatformColor objects to the platform ints Fabric expects; headless
|
|
144
|
+
// (no processor wired) it is the identity, so smokes see the input unchanged.
|
|
145
|
+
export function processColor(color) {
|
|
146
|
+
return colorProcessor(color);
|
|
147
|
+
}
|
|
148
|
+
// A color-keyed value the platform processor must convert before Fabric: a CSS
|
|
149
|
+
// string, or an opaque PlatformColor / DynamicColorIOS object. Numbers (already
|
|
150
|
+
// platform ints) and undefined are left untouched.
|
|
151
|
+
function isProcessableColor(value) {
|
|
152
|
+
return typeof value === 'string' || isOpaqueColorValue(value);
|
|
153
|
+
}
|
|
154
|
+
// Structured CSS-style keys RN parses in JS before native (boxShadow/filter register
|
|
155
|
+
// with enableNativeCSSParsing(), which DEFAULTS TO FALSE, so native CSS parsing is off
|
|
156
|
+
// and the raw string is dropped). Each runs on the hoisted top-level style key, turning
|
|
157
|
+
// a CSS string or structured array into the processed array Fabric's C++ expects.
|
|
158
|
+
const STYLE_PROCESSORS = new Map([
|
|
159
|
+
['boxShadow', value => processBoxShadow(asBoxShadowInput(value))],
|
|
160
|
+
['filter', value => processFilter(asFilterInput(value))],
|
|
161
|
+
['transformOrigin', value => processTransformOrigin(asTransformOriginInput(value))],
|
|
162
|
+
['transform', processTransformValue],
|
|
163
|
+
['aspectRatio', value => processAspectRatio(asAspectRatioInput(value))],
|
|
164
|
+
['fontVariant', value => processFontVariant(asFontVariantInput(value))],
|
|
165
|
+
['experimental_backgroundImage', value => processBackgroundImage(asBackgroundImageInput(value))],
|
|
166
|
+
]);
|
|
167
|
+
// boxShadow accepts a CSS string or an array of shadow objects; anything else is
|
|
168
|
+
// undefined to processBoxShadow (which returns []). Narrowing avoids an `as` cast.
|
|
169
|
+
function asBoxShadowInput(value) {
|
|
170
|
+
if (typeof value === 'string')
|
|
171
|
+
return value;
|
|
172
|
+
if (Array.isArray(value))
|
|
173
|
+
return value.filter(isRecord);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
// filter accepts a CSS string or an array of single-key filter objects; same narrowing.
|
|
177
|
+
function asFilterInput(value) {
|
|
178
|
+
if (typeof value === 'string')
|
|
179
|
+
return value;
|
|
180
|
+
if (Array.isArray(value))
|
|
181
|
+
return value.filter(isRecord);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
// experimental_backgroundImage accepts a CSS string (gradient functions) or an array of
|
|
185
|
+
// structured gradient objects; same narrowing as boxShadow/filter.
|
|
186
|
+
function asBackgroundImageInput(value) {
|
|
187
|
+
if (typeof value === 'string')
|
|
188
|
+
return value;
|
|
189
|
+
if (Array.isArray(value))
|
|
190
|
+
return value.filter(isRecord);
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
// transformOrigin accepts a CSS string or a [x, y, z] array of strings/numbers; anything
|
|
194
|
+
// else is undefined to processTransformOrigin (which defaults to center/center/0).
|
|
195
|
+
function asTransformOriginInput(value) {
|
|
196
|
+
if (typeof value === 'string')
|
|
197
|
+
return value;
|
|
198
|
+
if (Array.isArray(value))
|
|
199
|
+
return value.filter(isStringOrNumber);
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
// aspectRatio accepts a number (the common, working form) or a ratio string; otherwise
|
|
203
|
+
// undefined, which processAspectRatio drops.
|
|
204
|
+
function asAspectRatioInput(value) {
|
|
205
|
+
if (typeof value === 'number' || typeof value === 'string')
|
|
206
|
+
return value;
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
// fontVariant accepts an array of variant strings (the common, working form) or a
|
|
210
|
+
// space-separated string; anything else becomes an empty string, which yields [].
|
|
211
|
+
function asFontVariantInput(value) {
|
|
212
|
+
if (typeof value === 'string')
|
|
213
|
+
return value;
|
|
214
|
+
if (Array.isArray(value))
|
|
215
|
+
return value.filter(isString);
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
// transform accepts a CSS string (processTransform parses it) or an array of single-key
|
|
219
|
+
// transform records (the hot animated / sticky-header path, passed through unchanged).
|
|
220
|
+
// A non-string non-array value is NOT dropped: it may already be processed, so it passes
|
|
221
|
+
// through verbatim rather than being coerced to [] (which would erase a valid transform).
|
|
222
|
+
function processTransformValue(value) {
|
|
223
|
+
if (typeof value === 'string')
|
|
224
|
+
return processTransform(value);
|
|
225
|
+
if (Array.isArray(value))
|
|
226
|
+
return processTransform(value.filter(isRecord));
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
function isStringOrNumber(value) {
|
|
230
|
+
return typeof value === 'string' || typeof value === 'number';
|
|
231
|
+
}
|
|
232
|
+
function isString(value) {
|
|
233
|
+
return typeof value === 'string';
|
|
234
|
+
}
|
|
235
|
+
// Convert a prop to the shape Fabric's C++ expects. A third-party view contributes
|
|
236
|
+
// its own processors, auto-derived from its ViewConfig (validAttributes[*].process,
|
|
237
|
+
// e.g. processColor for a slider's track tints); those run first. Then the structured
|
|
238
|
+
// CSS-style processors (boxShadow/filter). Built-ins are never in the registry, so they
|
|
239
|
+
// fall through to the global color path, where any CSS-string color is run through the
|
|
240
|
+
// injected platform processor (Fabric's C++ color parser silently drops strings).
|
|
241
|
+
function processValue(component, key, value) {
|
|
242
|
+
const processor = registeredProcessor(component, key);
|
|
243
|
+
if (processor !== undefined)
|
|
244
|
+
return processor(value);
|
|
245
|
+
const styleProcessor = STYLE_PROCESSORS.get(key);
|
|
246
|
+
if (styleProcessor !== undefined)
|
|
247
|
+
return styleProcessor(value);
|
|
248
|
+
if (COLOR_PROPS.has(key) && isProcessableColor(value))
|
|
249
|
+
return colorProcessor(value);
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
function viewNameFor(node, hasTextAncestor) {
|
|
253
|
+
// The only position-dependent name: a <Text> inside another <Text> becomes a
|
|
254
|
+
// virtual span. Everything else is the component string the adapter chose.
|
|
255
|
+
return node.isText && hasTextAncestor ? VIRTUAL_TEXT_COMPONENT : node.component;
|
|
256
|
+
}
|
|
257
|
+
// Translate the retained node's logical props into the flat payload Fabric's C++
|
|
258
|
+
// props expect: `style` keys are hoisted to the top level, event handlers and
|
|
259
|
+
// undefined values are dropped.
|
|
260
|
+
function fabricProps(node) {
|
|
261
|
+
if (node.component === RAW_TEXT_COMPONENT) {
|
|
262
|
+
return { text: node.props.text };
|
|
263
|
+
}
|
|
264
|
+
const out = {};
|
|
265
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
266
|
+
if (key === 'style')
|
|
267
|
+
continue;
|
|
268
|
+
if (typeof value === 'function')
|
|
269
|
+
continue;
|
|
270
|
+
if (value === undefined)
|
|
271
|
+
continue;
|
|
272
|
+
out[key] = processValue(node.component, key, value);
|
|
273
|
+
}
|
|
274
|
+
// Collapse style (object | array | nested arrays) into one flat payload before
|
|
275
|
+
// hoisting: `style={[base, override]}` is RN's idiom and Fabric wants it flat.
|
|
276
|
+
const style = flattenStyle(node.props.style);
|
|
277
|
+
for (const [key, value] of Object.entries(style)) {
|
|
278
|
+
if (value !== undefined)
|
|
279
|
+
out[key] = processValue(node.component, key, value);
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
// Fabric's clone*WithNewProps MERGES the raw payload onto the node's existing props,
|
|
284
|
+
// so the payload must be a MINIMAL diff: only the keys that actually changed, plus any
|
|
285
|
+
// key the node held last time but no longer has, sent as `null` so Fabric resets it to
|
|
286
|
+
// default (e.g. `opacity` when a pressed style releases). Mirror React's diffProperties
|
|
287
|
+
// exactly: re-sending an UNCHANGED key is not a no-op, it re-invokes that prop's native
|
|
288
|
+
// setter, and some ViewManagers rebuild on any set. AndroidProgressBar's `styleAttr`
|
|
289
|
+
// setter recreates the whole ProgressBar via setStyle(), so re-sending it on an
|
|
290
|
+
// animating-only toggle dropped and rebuilt the spinner each time, and it never came
|
|
291
|
+
// back. Only matters for clones: a fresh createNode starts from nothing.
|
|
292
|
+
function diffProps(previous, next) {
|
|
293
|
+
const out = {};
|
|
294
|
+
for (const key of Object.keys(next)) {
|
|
295
|
+
if (!jsonEqual(previous[key], next[key]))
|
|
296
|
+
out[key] = next[key];
|
|
297
|
+
}
|
|
298
|
+
for (const key of Object.keys(previous)) {
|
|
299
|
+
if (!(key in next))
|
|
300
|
+
out[key] = null;
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
// Deep structural equality over the JSON-shaped props payload (Fabric props are
|
|
305
|
+
// serializable: primitives, arrays, plain objects). Used to decide whether a
|
|
306
|
+
// node's props actually changed: `fabricProps` builds a fresh object each
|
|
307
|
+
// commit, so a reference check would report every node as dirty.
|
|
308
|
+
function propsEqual(a, b) {
|
|
309
|
+
return jsonEqual(a, b);
|
|
310
|
+
}
|
|
311
|
+
function jsonEqual(a, b) {
|
|
312
|
+
if (Object.is(a, b))
|
|
313
|
+
return true;
|
|
314
|
+
const aArray = Array.isArray(a);
|
|
315
|
+
const bArray = Array.isArray(b);
|
|
316
|
+
if (aArray && bArray) {
|
|
317
|
+
if (a.length !== b.length)
|
|
318
|
+
return false;
|
|
319
|
+
return a.every((value, index) => jsonEqual(value, b[index]));
|
|
320
|
+
}
|
|
321
|
+
if (aArray || bArray)
|
|
322
|
+
return false;
|
|
323
|
+
if (!isRecord(a) || !isRecord(b))
|
|
324
|
+
return false;
|
|
325
|
+
const keys = Object.keys(a);
|
|
326
|
+
if (keys.length !== Object.keys(b).length)
|
|
327
|
+
return false;
|
|
328
|
+
return keys.every(key => key in b && jsonEqual(a[key], b[key]));
|
|
329
|
+
}
|
|
330
|
+
const mirror = new WeakMap();
|
|
331
|
+
function renderableChildren(node) {
|
|
332
|
+
// Anchor nodes (Vue fragment/v-if/v-for placeholders, Angular component hosts that should
|
|
333
|
+
// not paint) live in the retained tree for sibling ordering but never become Fabric views.
|
|
334
|
+
// When an anchor owns children, flatten them into the parent's renderable list: this lets a
|
|
335
|
+
// DOM-less framework use an anchor as a fragment/component host without adding a native
|
|
336
|
+
// wrapper node. Fast path: no anchors reuses the array, so the common case allocates nothing.
|
|
337
|
+
if (!node.children.some(isAnchor))
|
|
338
|
+
return node.children;
|
|
339
|
+
const children = [];
|
|
340
|
+
for (const child of node.children) {
|
|
341
|
+
if (isAnchor(child))
|
|
342
|
+
children.push(...renderableChildren(child));
|
|
343
|
+
else
|
|
344
|
+
children.push(child);
|
|
345
|
+
}
|
|
346
|
+
return children;
|
|
347
|
+
}
|
|
348
|
+
function childrenIdentical(kids, committed) {
|
|
349
|
+
if (kids.length !== committed.length)
|
|
350
|
+
return false;
|
|
351
|
+
return kids.every((child, index) => child === committed[index]);
|
|
352
|
+
}
|
|
353
|
+
// Diagnostic seam (gated): a ScrollView on Android must hold exactly ONE direct
|
|
354
|
+
// child (its content container), or the native mount aborts with "ScrollView can
|
|
355
|
+
// host only one direct child". Logged after children reconcile so each child's
|
|
356
|
+
// committed tag/view-name is resolved. A `MULTI!!` line names the exact extra
|
|
357
|
+
// node (tag + view-name) that pushed the scroll view past one child.
|
|
358
|
+
function logScrollChildren(node, viewName, selfTag) {
|
|
359
|
+
if (!viewName.includes('Scroll') || viewName.includes('Content'))
|
|
360
|
+
return;
|
|
361
|
+
const kids = node.children.map(child => {
|
|
362
|
+
const committed = mirror.get(child);
|
|
363
|
+
return `${committed?.viewName ?? child.component}#${committed?.tag ?? 'NEW'}`;
|
|
364
|
+
});
|
|
365
|
+
const flag = kids.length === 1 ? 'OK' : 'MULTI!!';
|
|
366
|
+
dlog(`SCROLL-${flag} ${viewName} tag=${selfTag} children(${kids.length})=[${kids.join(',')}]`);
|
|
367
|
+
}
|
|
368
|
+
function reconcile(slot, node, rootTag, hasTextAncestor, renderableParent, forceFreshFamily) {
|
|
369
|
+
const viewName = viewNameFor(node, hasTextAncestor);
|
|
370
|
+
const props = fabricProps(node);
|
|
371
|
+
const childInText = node.isText || hasTextAncestor;
|
|
372
|
+
const committed = mirror.get(node);
|
|
373
|
+
// The children that actually reach Fabric. Anchors are filtered out here so the
|
|
374
|
+
// whole walk (child-set emission, identity diff, mirror) is anchor-blind.
|
|
375
|
+
const kids = renderableChildren(node);
|
|
376
|
+
// First mount, or the view kind flipped (RCTText <-> RCTVirtualText when a
|
|
377
|
+
// <Text> moves in or out of another <Text>): a different native component
|
|
378
|
+
// can't be cloned across, so create a fresh node from scratch.
|
|
379
|
+
const parentChanged = committed !== undefined && committed.parent !== renderableParent;
|
|
380
|
+
if (forceFreshFamily ||
|
|
381
|
+
committed === undefined ||
|
|
382
|
+
committed.viewName !== viewName ||
|
|
383
|
+
parentChanged) {
|
|
384
|
+
stats.created += 1;
|
|
385
|
+
const tag = nextTag();
|
|
386
|
+
const reason = committed === undefined
|
|
387
|
+
? 'mount'
|
|
388
|
+
: forceFreshFamily
|
|
389
|
+
? 'fresh-parent'
|
|
390
|
+
: committed.viewName !== viewName
|
|
391
|
+
? 'view-kind'
|
|
392
|
+
: 'reparent';
|
|
393
|
+
dlog(`commit root=${rootTag} createNode tag=${tag} view=${viewName} reason=${reason}`);
|
|
394
|
+
if (viewName === 'RCTView' || viewName === 'RCTText') {
|
|
395
|
+
dlog(`commit root=${rootTag} colorProbe tag=${tag} view=${viewName} ` +
|
|
396
|
+
`bg=${JSON.stringify(props.backgroundColor)} color=${JSON.stringify(props.color)} ` +
|
|
397
|
+
`opacity=${JSON.stringify(props.opacity)}`);
|
|
398
|
+
}
|
|
399
|
+
if (viewName === 'AndroidSwipeRefreshLayout' || viewName === 'RCTScrollView') {
|
|
400
|
+
dlog(`commit root=${rootTag} layoutProbe tag=${tag} view=${viewName} ` +
|
|
401
|
+
`flex=${JSON.stringify(props.flex)} height=${JSON.stringify(props.height)} ` +
|
|
402
|
+
`width=${JSON.stringify(props.width)} minHeight=${JSON.stringify(props.minHeight)} ` +
|
|
403
|
+
`flexGrow=${JSON.stringify(props.flexGrow)}`);
|
|
404
|
+
}
|
|
405
|
+
const handle = slot.createNode(tag, viewName, rootTag, props, node);
|
|
406
|
+
for (const child of kids) {
|
|
407
|
+
slot.appendChild(handle, reconcile(slot, child, rootTag, childInText, node, true).handle);
|
|
408
|
+
}
|
|
409
|
+
logScrollChildren(node, viewName, tag);
|
|
410
|
+
mirror.set(node, {
|
|
411
|
+
handle,
|
|
412
|
+
tag,
|
|
413
|
+
rootTag,
|
|
414
|
+
props,
|
|
415
|
+
children: kids.slice(),
|
|
416
|
+
viewName,
|
|
417
|
+
parent: renderableParent,
|
|
418
|
+
});
|
|
419
|
+
return { handle, changed: true };
|
|
420
|
+
}
|
|
421
|
+
// Reconcile children first; a child that re-cloned forces this node to re-clone
|
|
422
|
+
// too, since Fabric parents point at specific child handles.
|
|
423
|
+
const childHandles = [];
|
|
424
|
+
let descendantChanged = false;
|
|
425
|
+
for (const child of kids) {
|
|
426
|
+
const result = reconcile(slot, child, rootTag, childInText, node, false);
|
|
427
|
+
childHandles.push(result.handle);
|
|
428
|
+
if (result.changed)
|
|
429
|
+
descendantChanged = true;
|
|
430
|
+
}
|
|
431
|
+
logScrollChildren(node, viewName, committed.tag);
|
|
432
|
+
const childrenChanged = !childrenIdentical(kids, committed.children) || descendantChanged;
|
|
433
|
+
const propsChanged = !propsEqual(committed.props, props);
|
|
434
|
+
if (!childrenChanged && !propsChanged) {
|
|
435
|
+
stats.reused += 1;
|
|
436
|
+
return { handle: committed.handle, changed: false };
|
|
437
|
+
}
|
|
438
|
+
let handle;
|
|
439
|
+
if (childrenChanged) {
|
|
440
|
+
stats.cloneChildren += 1;
|
|
441
|
+
if (propsChanged) {
|
|
442
|
+
const propsDiff = diffProps(committed.props, props);
|
|
443
|
+
guardSerializable(propsDiff, viewName, committed.tag);
|
|
444
|
+
handle = slot.cloneNodeWithNewChildrenAndProps(committed.handle, propsDiff);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
handle = slot.cloneNodeWithNewChildren(committed.handle);
|
|
448
|
+
}
|
|
449
|
+
for (const childHandle of childHandles) {
|
|
450
|
+
slot.appendChild(handle, childHandle);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
stats.cloneProps += 1;
|
|
455
|
+
const propsDiff = diffProps(committed.props, props);
|
|
456
|
+
guardSerializable(propsDiff, viewName, committed.tag);
|
|
457
|
+
handle = slot.cloneNodeWithNewProps(committed.handle, propsDiff);
|
|
458
|
+
}
|
|
459
|
+
// The clone keeps the node's family, so its reactTag is unchanged; carry it.
|
|
460
|
+
mirror.set(node, {
|
|
461
|
+
handle,
|
|
462
|
+
tag: committed.tag,
|
|
463
|
+
rootTag,
|
|
464
|
+
props,
|
|
465
|
+
// Store the same flattened child list we diffed against. Anchors are retained-tree
|
|
466
|
+
// bookkeeping only; keeping raw node.children here makes every anchored subtree look
|
|
467
|
+
// structurally changed on the next commit and can re-append already-parented Fabric
|
|
468
|
+
// ShadowNode families under a cloned parent.
|
|
469
|
+
children: kids.slice(),
|
|
470
|
+
viewName,
|
|
471
|
+
parent: renderableParent,
|
|
472
|
+
});
|
|
473
|
+
return { handle, changed: true };
|
|
474
|
+
}
|
|
475
|
+
// One persistent synthetic root container per surface, mirroring RN's AppContainer
|
|
476
|
+
// (renderApplication wraps the app in `<View style={{flex:1}} pointerEvents="box-none">`).
|
|
477
|
+
// Without it a non-flex root view collapses to content height, and touches outside the
|
|
478
|
+
// app's children have no box-none escape. Keeping it here (not in each adapter's
|
|
479
|
+
// mount()) gives every framework a full-screen flex root for free and keeps layout in
|
|
480
|
+
// shared (adapters_stay_thin). The container is just another persistent node in the
|
|
481
|
+
// clone-on-write engine: stable identity, so an unchanged subtree leaves it un-cloned.
|
|
482
|
+
const ROOT_VIEW_COMPONENT = 'RCTView';
|
|
483
|
+
const ROOT_CONTAINER_STYLE = { flex: 1 };
|
|
484
|
+
const ROOT_CONTAINER_POINTER_EVENTS = 'box-none';
|
|
485
|
+
const rootContainers = new Map();
|
|
486
|
+
function rootContainerFor(rootTag) {
|
|
487
|
+
let container = rootContainers.get(rootTag);
|
|
488
|
+
if (container === undefined) {
|
|
489
|
+
container = createElement(ROOT_VIEW_COMPONENT);
|
|
490
|
+
container.props = {
|
|
491
|
+
style: ROOT_CONTAINER_STYLE,
|
|
492
|
+
pointerEvents: ROOT_CONTAINER_POINTER_EVENTS,
|
|
493
|
+
};
|
|
494
|
+
rootContainers.set(rootTag, container);
|
|
495
|
+
dlog(`root container created root=${rootTag} (flex:1, box-none)`);
|
|
496
|
+
}
|
|
497
|
+
return container;
|
|
498
|
+
}
|
|
499
|
+
// Drop a surface's persistent root container so the NEXT mount on this rootTag starts
|
|
500
|
+
// from scratch (fresh tags, fresh mirror) instead of cloning handles that belonged to a
|
|
501
|
+
// now-stopped surface. Called from unmount (the bridgeless surface-stop path): the host stops then restarts a
|
|
502
|
+
// surface (Fast Refresh, focus/lifecycle) reusing the same rootTag, and a stale root
|
|
503
|
+
// container would re-clone dead handles into the new surface → a blank screen. The old
|
|
504
|
+
// container's descendants fall out of every reference and their mirror entries GC.
|
|
505
|
+
export function disposeRoot(rootTag) {
|
|
506
|
+
if (rootContainers.delete(rootTag))
|
|
507
|
+
dlog(`root container disposed root=${rootTag}`);
|
|
508
|
+
}
|
|
509
|
+
export function commitChildren(rootTag, children) {
|
|
510
|
+
// The wrapper holds the surface's top-level children; reconcile walks from it so the
|
|
511
|
+
// whole tree, synthetic root included, goes through the same clone-on-write path.
|
|
512
|
+
rootContainerFor(rootTag).children = children.slice();
|
|
513
|
+
commitContainer(rootTag);
|
|
514
|
+
}
|
|
515
|
+
// Re-run the scoped commit for a surface from its synthetic root container, reusing
|
|
516
|
+
// whatever top-level children it currently holds. The shared half of the engine: both
|
|
517
|
+
// a full mutation→commit and a single-node Animated frame (setNativeProps) funnel here.
|
|
518
|
+
function commitContainer(rootTag) {
|
|
519
|
+
const slot = getSlot();
|
|
520
|
+
const container = rootContainerFor(rootTag);
|
|
521
|
+
stats.created = 0;
|
|
522
|
+
stats.cloneProps = 0;
|
|
523
|
+
stats.cloneChildren = 0;
|
|
524
|
+
stats.reused = 0;
|
|
525
|
+
// Entry seam: brackets reconcile with the `reconciled` line below. If `start` prints
|
|
526
|
+
// but `reconciled` never does, the stall is inside reconcile (a JS loop/cycle in the
|
|
527
|
+
// tree walk); if `start` itself never prints, the stall is upstream: React's commit
|
|
528
|
+
// phase or the mutation ops before we are even called.
|
|
529
|
+
dlog(`commit root=${rootTag} start children=${container.children.length}`);
|
|
530
|
+
const result = reconcile(slot, container, rootTag, false, undefined, false);
|
|
531
|
+
// Boundary seam: prints once reconcile returns. If a commit hangs and this line
|
|
532
|
+
// never appears, the stall is inside reconcile (JS); if it appears but the
|
|
533
|
+
// post-completeRoot line below never does, the stall is inside the native commit.
|
|
534
|
+
dlog(`commit root=${rootTag} reconciled changed=${result.changed}`);
|
|
535
|
+
// The container's identity is stable, so its un-cloned flag is the no-op signal:
|
|
536
|
+
// an over-scheduled commit that touched nothing makes zero native calls.
|
|
537
|
+
if (!result.changed) {
|
|
538
|
+
dlog(`commit root=${rootTag} no-op (skipped completeRoot)`);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const childSet = slot.createChildSet(rootTag);
|
|
542
|
+
slot.appendChildToSet(childSet, result.handle);
|
|
543
|
+
dlog(`commit root=${rootTag} pre-completeRoot`);
|
|
544
|
+
slot.completeRoot(rootTag, childSet);
|
|
545
|
+
// Fresh Fabric tags are now assigned: let any consumer that needed a committed tag
|
|
546
|
+
// and ran too early (the Animated native driver binding a props node to a view under
|
|
547
|
+
// an async-batched commit) retry now. No-op when nothing is pending.
|
|
548
|
+
runPostCommitHooks();
|
|
549
|
+
if (isDebug()) {
|
|
550
|
+
const mode = stats.created > 0 && stats.reused === 0 ? 'full' : 'incremental';
|
|
551
|
+
dlog(`commit root=${rootTag} ${mode} ` +
|
|
552
|
+
`created=${stats.created} cloneProps=${stats.cloneProps} ` +
|
|
553
|
+
`cloneChildren=${stats.cloneChildren} reused=${stats.reused}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Targeted per-frame prop write for the JS-driven Animated path (ADR 0016). RN
|
|
557
|
+
// flushes an animation frame with an in-place `instance.setNativeProps(...)`; we have
|
|
558
|
+
// no in-place mutation (Fabric is persistent), so a frame is one scoped commit: mutate
|
|
559
|
+
// the node's desired props, then re-reconcile its surface. The engine clones only this
|
|
560
|
+
// node (props differ), bubbles the re-clone to the root, reuses every sibling subtree
|
|
561
|
+
// by reference, and emits a single completeRoot. This is the "slow tier", viable for a
|
|
562
|
+
// single shallow animation; the native driver (ADR 0017) is the answer for scale.
|
|
563
|
+
export function setNativeProps(node, partial) {
|
|
564
|
+
const record = mirror.get(node);
|
|
565
|
+
if (record === undefined) {
|
|
566
|
+
dlog('setNativeProps skipped: node not committed');
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
for (const [key, value] of Object.entries(partial)) {
|
|
570
|
+
if (key === 'style') {
|
|
571
|
+
// A partial style override MERGES onto the declarative style (RN semantics):
|
|
572
|
+
// setNativeProps({style:{backgroundColor}}) recolors without dropping height
|
|
573
|
+
// or radius. Transient: the next React commit re-applies the full style.
|
|
574
|
+
node.props.style = { ...flattenStyle(node.props.style), ...flattenStyle(value) };
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
node.props[key] = value;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
dlog(`setNativeProps root=${record.rootTag} tag=${record.tag} keys=${Object.keys(partial)}`);
|
|
581
|
+
commitContainer(record.rootTag);
|
|
582
|
+
}
|
|
583
|
+
// The committed reactTag of a node (stable across clone-on-write), for binding the
|
|
584
|
+
// Animated native driver via connectAnimatedNodeToView (ADR 0017). Undefined until the
|
|
585
|
+
// node has been committed at least once.
|
|
586
|
+
export function getNativeTag(node) {
|
|
587
|
+
return mirror.get(node)?.tag;
|
|
588
|
+
}
|
|
589
|
+
// Actions waiting for their node's first commit. An adapter that wires an imperative/native call at
|
|
590
|
+
// lifecycle time (autoFocus, a native Animated.event attach) can run BEFORE completeRoot under an
|
|
591
|
+
// async-batched commit (Vue/Svelte schedule it on a microtask), so the node has no tag yet and the
|
|
592
|
+
// call silently no-ops. Each waiter retries after a commit may have assigned the tag and is dropped
|
|
593
|
+
// once it runs. React commits synchronously, so its actions run inline and never land here.
|
|
594
|
+
const pendingCommitWaiters = new Set();
|
|
595
|
+
registerPostCommit(() => {
|
|
596
|
+
for (const waiter of pendingCommitWaiters) {
|
|
597
|
+
if (waiter())
|
|
598
|
+
pendingCommitWaiters.delete(waiter);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
// Run `action` once `node` has a committed Fabric tag — immediately if it already does, else after
|
|
602
|
+
// the commit that assigns it. The canonical fix for the Vue async-commit race: defer instead of
|
|
603
|
+
// silently no-opping. Returns a cancel fn (drop the pending retry, e.g. on unmount).
|
|
604
|
+
export function whenCommitted(node, action) {
|
|
605
|
+
const attempt = () => {
|
|
606
|
+
if (mirror.get(node) === undefined)
|
|
607
|
+
return false;
|
|
608
|
+
action();
|
|
609
|
+
return true;
|
|
610
|
+
};
|
|
611
|
+
if (!attempt())
|
|
612
|
+
pendingCommitWaiters.add(attempt);
|
|
613
|
+
return () => {
|
|
614
|
+
pendingCommitWaiters.delete(attempt);
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
// The node's current Fabric handle (the createNode/clone return value), identical in
|
|
618
|
+
// kind to React's stateNode.node, for the native driver's ShadowNodeFamily path.
|
|
619
|
+
export function getNativeNode(node) {
|
|
620
|
+
return mirror.get(node)?.handle;
|
|
621
|
+
}
|
|
622
|
+
// Imperative view command (e.g. TextInput's setTextAndSelection / focus / blur),
|
|
623
|
+
// aimed at a node's CURRENT Fabric handle. Only valid once the node has been
|
|
624
|
+
// committed at least once; its handle is read from the mirror.
|
|
625
|
+
export function dispatchViewCommand(node, commandName, args) {
|
|
626
|
+
const record = mirror.get(node);
|
|
627
|
+
if (record === undefined) {
|
|
628
|
+
dlog(`dispatchViewCommand "${commandName}" skipped: node not committed`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
dlog(`dispatchViewCommand "${commandName}"`);
|
|
632
|
+
getSlot().dispatchCommand(record.handle, commandName, args);
|
|
633
|
+
}
|
|
634
|
+
// Emit an accessibility event (focus/click/viewHoverEnter/windowStateChange) at a node's
|
|
635
|
+
// CURRENT Fabric handle, routed through the slot exactly like dispatchViewCommand. RN's
|
|
636
|
+
// Fabric path hands the public-instance handle to nativeFabricUIManager.sendAccessibilityEvent
|
|
637
|
+
// with the STRING eventType; the C++ side maps it to the platform's accessibility-event kind.
|
|
638
|
+
// A no-op (logged) until the node is committed; there is no handle yet.
|
|
639
|
+
export function sendAccessibilityEvent(node, eventType) {
|
|
640
|
+
const record = mirror.get(node);
|
|
641
|
+
if (record === undefined) {
|
|
642
|
+
dlog(`sendAccessibilityEvent "${eventType}" skipped: node not committed`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
dlog(`sendAccessibilityEvent "${eventType}"`);
|
|
646
|
+
getSlot().sendAccessibilityEvent(record.handle, eventType);
|
|
647
|
+
}
|
|
648
|
+
// Imperative measurement against a node's CURRENT Fabric handle (the public-instance
|
|
649
|
+
// measure family that reanimated / gesture-handler / scroll-to reach through). A
|
|
650
|
+
// no-op with a dlog until the node is committed; there is no handle to measure yet.
|
|
651
|
+
export function measure(node, callback) {
|
|
652
|
+
const record = mirror.get(node);
|
|
653
|
+
if (record === undefined) {
|
|
654
|
+
dlog('measure skipped: node not committed');
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
getSlot().measure(record.handle, callback);
|
|
658
|
+
}
|
|
659
|
+
export function measureInWindow(node, callback) {
|
|
660
|
+
const record = mirror.get(node);
|
|
661
|
+
if (record === undefined) {
|
|
662
|
+
dlog('measureInWindow skipped: node not committed');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
getSlot().measureInWindow(record.handle, callback);
|
|
666
|
+
}
|
|
667
|
+
// Measure `node`'s frame relative to `relativeTo`. Both must be committed; RN's public
|
|
668
|
+
// signature is (relative, onSuccess, onFail) but the native slot wants the fail
|
|
669
|
+
// callback before success, so the order is swapped here.
|
|
670
|
+
export function measureLayout(node, relativeTo, onSuccess, onFail = () => { }) {
|
|
671
|
+
const record = mirror.get(node);
|
|
672
|
+
const relativeRecord = mirror.get(relativeTo);
|
|
673
|
+
if (record === undefined || relativeRecord === undefined) {
|
|
674
|
+
dlog('measureLayout skipped: a node is not committed');
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
getSlot().measureLayout(record.handle, relativeRecord.handle, onFail, onSuccess);
|
|
678
|
+
}
|