@ccheever/exact-renderer 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/package.json +118 -0
- package/src/__tests__/adapter-window-state.test.tsx +190 -0
- package/src/__tests__/attrs.test.ts +157 -0
- package/src/__tests__/classname.test.ts +332 -0
- package/src/__tests__/color.test.ts +169 -0
- package/src/__tests__/dom-mirror.test.ts +682 -0
- package/src/__tests__/dom-shim.test.ts +274 -0
- package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
- package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
- package/src/__tests__/host-config.test.ts +51 -0
- package/src/__tests__/host-ops.test.ts +2234 -0
- package/src/__tests__/image-source.test.ts +135 -0
- package/src/__tests__/liquid-glass.test.ts +72 -0
- package/src/__tests__/multi-root.test.ts +118 -0
- package/src/__tests__/native-view-events.test.ts +102 -0
- package/src/__tests__/nodes.test.ts +399 -0
- package/src/__tests__/normalize.test.ts +576 -0
- package/src/__tests__/paragraph-lowering.test.tsx +144 -0
- package/src/__tests__/props.test.ts +518 -0
- package/src/__tests__/protocol-encoder.test.ts +732 -0
- package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
- package/src/__tests__/reconciler.test.tsx +241 -0
- package/src/__tests__/svelte-adapter.test.ts +166 -0
- package/src/__tests__/svg-source.test.ts +71 -0
- package/src/__tests__/tags.test.ts +354 -0
- package/src/__tests__/toggle.test.ts +441 -0
- package/src/__tests__/transitions.test.ts +106 -0
- package/src/__tests__/web-primitives.test.tsx +454 -0
- package/src/__tests__/window-hooks.test.tsx +447 -0
- package/src/adapter-contract.ts +68 -0
- package/src/attrs.ts +596 -0
- package/src/classname-contract.ts +87 -0
- package/src/classname-resolve.ts +553 -0
- package/src/classname-runtime.ts +29 -0
- package/src/components.ts +214 -0
- package/src/css-variable-context.ts +83 -0
- package/src/dom-hydration.ts +160 -0
- package/src/dom-mirror.ts +1459 -0
- package/src/dom-shim.ts +1736 -0
- package/src/group-context.ts +69 -0
- package/src/host-config.ts +431 -0
- package/src/host-ops.ts +3167 -0
- package/src/image-source.native.ts +703 -0
- package/src/image-source.ts +554 -0
- package/src/index.ts +278 -0
- package/src/inspector-runtime.ts +244 -0
- package/src/inspector.ts +3570 -0
- package/src/jsx-augmentations.ts +54 -0
- package/src/keyboard-avoidance.ts +217 -0
- package/src/native-primitives.ts +43 -0
- package/src/native-view-events.ts +322 -0
- package/src/native-view.ts +60 -0
- package/src/nodes/index.ts +41 -0
- package/src/nodes/node.ts +531 -0
- package/src/peer-context.ts +100 -0
- package/src/primitives.native.ts +8 -0
- package/src/primitives.ts +8 -0
- package/src/props/index.ts +14 -0
- package/src/props/normalize.ts +816 -0
- package/src/protocol/encoder.ts +940 -0
- package/src/protocol/index.ts +33 -0
- package/src/reconciler.ts +581 -0
- package/src/runtime.ts +11 -0
- package/src/safe-area.ts +543 -0
- package/src/solid.ts +490 -0
- package/src/style/color.js +1 -0
- package/src/style/color.ts +15 -0
- package/src/style/index.js +1 -0
- package/src/style/index.ts +22 -0
- package/src/style/normalize.js +1 -0
- package/src/style/normalize.ts +1426 -0
- package/src/svelte.ts +349 -0
- package/src/svg-source.ts +222 -0
- package/src/tags/index.ts +21 -0
- package/src/tags/tag-map.ts +289 -0
- package/src/text/paragraph-lowering.ts +310 -0
- package/src/types.ts +1175 -0
- package/src/vue.ts +535 -0
- package/src/web-host.ts +19 -0
- package/src/web-primitives.ts +1654 -0
package/src/host-ops.ts
ADDED
|
@@ -0,0 +1,3167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host Operations - Shared Infrastructure
|
|
3
|
+
*
|
|
4
|
+
* This module provides framework-agnostic operations for the Exact renderer.
|
|
5
|
+
* It is used by React, Solid, and Vue adapters to:
|
|
6
|
+
* - Encode protocol messages to native
|
|
7
|
+
* - Manage event handler registry
|
|
8
|
+
* - Create and manipulate nodes
|
|
9
|
+
*
|
|
10
|
+
* Design Principles:
|
|
11
|
+
* - Framework-agnostic: no React/Solid/Vue dependencies
|
|
12
|
+
* - Single source of truth for protocol encoding
|
|
13
|
+
* - Shared event handler registry across all frameworks
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ElementNode, TextNode, RootNode } from './nodes/node.js';
|
|
17
|
+
import type { ParagraphSpec } from '@exact/text';
|
|
18
|
+
import {
|
|
19
|
+
EventType,
|
|
20
|
+
NodeType,
|
|
21
|
+
} from '@exact/core/protocol/opcodes';
|
|
22
|
+
import {
|
|
23
|
+
clearFocusedTarget,
|
|
24
|
+
setFocusedTarget,
|
|
25
|
+
} from '@exact/core/agent/interaction-state';
|
|
26
|
+
import { recordInboundProtocolEvent, resetProtocolTapForTests } from '@exact/core/agent';
|
|
27
|
+
import {
|
|
28
|
+
detectTextDirection,
|
|
29
|
+
} from '@exact/core/i18n/helpers';
|
|
30
|
+
import {
|
|
31
|
+
getExactAccessibilitySnapshot,
|
|
32
|
+
subscribeExactAccessibilityChanges,
|
|
33
|
+
} from '@exact/core/runtime/accessibility-state';
|
|
34
|
+
import {
|
|
35
|
+
getExactLocaleSnapshot,
|
|
36
|
+
subscribeExactLocaleChanges,
|
|
37
|
+
} from '@exact/core/runtime/locale-state';
|
|
38
|
+
import type {
|
|
39
|
+
CanonicalTagType,
|
|
40
|
+
FullStyle,
|
|
41
|
+
CanonicalStyle,
|
|
42
|
+
CanonicalProps,
|
|
43
|
+
CanonicalTransitionMap,
|
|
44
|
+
FlexDirection,
|
|
45
|
+
Direction,
|
|
46
|
+
ResolvedDirection,
|
|
47
|
+
SafeAreaPropagationState,
|
|
48
|
+
WritingMode,
|
|
49
|
+
} from './types.js';
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
DirtyFlags,
|
|
53
|
+
NodeKind,
|
|
54
|
+
clearDirtyFlags,
|
|
55
|
+
createElementNode,
|
|
56
|
+
createTextNode,
|
|
57
|
+
createRootNode,
|
|
58
|
+
hasDirtyFlag,
|
|
59
|
+
appendChild as nodeAppendChild,
|
|
60
|
+
insertBefore as nodeInsertBefore,
|
|
61
|
+
removeChild as nodeRemoveChild,
|
|
62
|
+
clearChildren as nodeClearChildren,
|
|
63
|
+
} from './nodes/node.js';
|
|
64
|
+
|
|
65
|
+
import { getTagConfig, defaultTagConfig, type TagConfig } from './tags/index.js';
|
|
66
|
+
import {
|
|
67
|
+
registerNativeViewEventHandler,
|
|
68
|
+
unregisterNativeViewEventHandler,
|
|
69
|
+
} from './native-view-events.js';
|
|
70
|
+
import {
|
|
71
|
+
resolveClassNameStyle,
|
|
72
|
+
type ClassNameResolutionContext,
|
|
73
|
+
} from './classname-runtime.js';
|
|
74
|
+
import { normalizeStyle, normalizeTransition, stylesEqual, transitionsEqual } from './style/index.js';
|
|
75
|
+
import { normalizeProps, propsEqual } from './props/index.js';
|
|
76
|
+
import { createRootSafeAreaPropagation, resolveSafeAreaForElement } from './safe-area.js';
|
|
77
|
+
import {
|
|
78
|
+
clearFocusedInput,
|
|
79
|
+
noteFocusedInput,
|
|
80
|
+
resetKeyboardAvoidanceState,
|
|
81
|
+
syncKeyboardAvoidanceAfterCommit,
|
|
82
|
+
} from './keyboard-avoidance.js';
|
|
83
|
+
import {
|
|
84
|
+
type ProtocolEncoder,
|
|
85
|
+
createProtocolEncoder,
|
|
86
|
+
setStyle,
|
|
87
|
+
setTransform,
|
|
88
|
+
setOpacity,
|
|
89
|
+
setBackgroundColor,
|
|
90
|
+
setTransition,
|
|
91
|
+
setLang,
|
|
92
|
+
setTextContent,
|
|
93
|
+
dispatchProtocol,
|
|
94
|
+
getScreenDimensions,
|
|
95
|
+
encodeCreateElement,
|
|
96
|
+
encodeCreateText,
|
|
97
|
+
encodeProps,
|
|
98
|
+
encodeEvents,
|
|
99
|
+
encodeChildren,
|
|
100
|
+
encodeDestroy,
|
|
101
|
+
} from './protocol/index.js';
|
|
102
|
+
import { notifyCommit, registerRoot, unregisterRoot } from './inspector-runtime.js';
|
|
103
|
+
import { lowerTextPropsToParagraphSpec } from './text/paragraph-lowering.js';
|
|
104
|
+
|
|
105
|
+
// =============================================================================
|
|
106
|
+
// Development Mode Flag
|
|
107
|
+
// =============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Development mode flag - gate verbose logging.
|
|
111
|
+
* In production builds, bundlers should replace this with false.
|
|
112
|
+
*/
|
|
113
|
+
const __DEV__ = process.env.NODE_ENV !== 'production';
|
|
114
|
+
const warnedAutoDirectionTags = new Set<string>();
|
|
115
|
+
const liveRoots = new Map<number, RootNode>();
|
|
116
|
+
const rootOwners = new Map<number, string>();
|
|
117
|
+
const committedRootViewportSizes = new Map<number, { width: number; height: number }>();
|
|
118
|
+
const pendingCommitStateByRoot = new Map<number, PendingCommitState>();
|
|
119
|
+
let accessibilityChangeUnsubscribe: (() => void) | null = null;
|
|
120
|
+
let localeChangeUnsubscribe: (() => void) | null = null;
|
|
121
|
+
|
|
122
|
+
interface InheritedElementState {
|
|
123
|
+
rootId: number;
|
|
124
|
+
direction: ResolvedDirection;
|
|
125
|
+
writingMode: WritingMode;
|
|
126
|
+
lang: string;
|
|
127
|
+
safeArea: SafeAreaPropagationState;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface ResolvedElementState {
|
|
131
|
+
props: CanonicalProps;
|
|
132
|
+
paragraphSpec: ParagraphSpec | null;
|
|
133
|
+
paragraphKey: string | null;
|
|
134
|
+
style: CanonicalStyle;
|
|
135
|
+
transitions: CanonicalTransitionMap;
|
|
136
|
+
resolvedDirection: ResolvedDirection;
|
|
137
|
+
resolvedWritingMode: WritingMode;
|
|
138
|
+
resolvedLang: string;
|
|
139
|
+
safeAreaState: ElementNode['safeAreaState'];
|
|
140
|
+
propagatedSafeArea: ElementNode['propagatedSafeArea'];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface PendingCommitState {
|
|
144
|
+
layoutDirty: boolean;
|
|
145
|
+
warningAnalysisDirty: boolean;
|
|
146
|
+
dirtyChildren: Set<RootNode | ElementNode>;
|
|
147
|
+
changedViews: Set<number>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const SINGLE_LINE_INPUT_MIN_HEIGHT = 36;
|
|
151
|
+
const TOGGLE_NATIVE_VIEW_MIN_WIDTH = 46;
|
|
152
|
+
const TOGGLE_NATIVE_VIEW_MIN_HEIGHT = 28;
|
|
153
|
+
|
|
154
|
+
function applyPropDrivenStyleOverrides(
|
|
155
|
+
tagType: CanonicalTagType,
|
|
156
|
+
style: CanonicalStyle,
|
|
157
|
+
props: CanonicalProps,
|
|
158
|
+
originalProps: Props,
|
|
159
|
+
): CanonicalStyle {
|
|
160
|
+
let next = style;
|
|
161
|
+
|
|
162
|
+
if ((tagType === 'image' || tagType === 'svg') && props.resizeMode !== undefined) {
|
|
163
|
+
next = {
|
|
164
|
+
...next,
|
|
165
|
+
resizeMode: props.resizeMode,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (tagType === 'input' && props.multiline === true) {
|
|
170
|
+
const lifted = normalizeStyle(
|
|
171
|
+
{ numberOfLines: originalProps.numberOfLines ?? 2 } as FullStyle,
|
|
172
|
+
'row',
|
|
173
|
+
);
|
|
174
|
+
if (lifted.numberOfLines !== undefined) {
|
|
175
|
+
next = {
|
|
176
|
+
...next,
|
|
177
|
+
numberOfLines: lifted.numberOfLines,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
tagType === 'input' &&
|
|
184
|
+
props.multiline !== true &&
|
|
185
|
+
next.height === undefined &&
|
|
186
|
+
next.minHeight === undefined
|
|
187
|
+
) {
|
|
188
|
+
next = {
|
|
189
|
+
...next,
|
|
190
|
+
minHeight: { type: 'points', value: SINGLE_LINE_INPUT_MIN_HEIGHT },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (tagType === 'nativeView' && props.nativeViewModuleName === 'exact.toggle') {
|
|
195
|
+
if (next.width === undefined && next.minWidth === undefined) {
|
|
196
|
+
next = {
|
|
197
|
+
...next,
|
|
198
|
+
minWidth: { type: 'points', value: TOGGLE_NATIVE_VIEW_MIN_WIDTH },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (next.height === undefined && next.minHeight === undefined) {
|
|
202
|
+
next = {
|
|
203
|
+
...next,
|
|
204
|
+
minHeight: { type: 'points', value: TOGGLE_NATIVE_VIEW_MIN_HEIGHT },
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return next;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function applyTextLayoutPropStyleOverrides(
|
|
213
|
+
tagType: CanonicalTagType,
|
|
214
|
+
style: CanonicalStyle,
|
|
215
|
+
props: Props,
|
|
216
|
+
): CanonicalStyle {
|
|
217
|
+
if (tagType !== 'text') {
|
|
218
|
+
return style;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let next = style;
|
|
222
|
+
if (props.numberOfLines !== undefined) {
|
|
223
|
+
const lifted = normalizeStyle(
|
|
224
|
+
{ numberOfLines: props.numberOfLines } as FullStyle,
|
|
225
|
+
'row',
|
|
226
|
+
);
|
|
227
|
+
if (lifted.numberOfLines !== undefined) {
|
|
228
|
+
next = { ...next, numberOfLines: lifted.numberOfLines };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (props.ellipsizeMode !== undefined) {
|
|
232
|
+
const lifted = normalizeStyle(
|
|
233
|
+
{ ellipsizeMode: props.ellipsizeMode } as FullStyle,
|
|
234
|
+
'row',
|
|
235
|
+
);
|
|
236
|
+
if (lifted.ellipsizeMode !== undefined) {
|
|
237
|
+
next = { ...next, ellipsizeMode: lifted.ellipsizeMode };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return next;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function applyAccessibilityDrivenStyleOverrides(
|
|
244
|
+
style: CanonicalStyle,
|
|
245
|
+
props: CanonicalProps,
|
|
246
|
+
): CanonicalStyle {
|
|
247
|
+
if (style.fontSize === undefined || props.allowFontScaling === false) {
|
|
248
|
+
return style;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const accessibility = getExactAccessibilitySnapshot();
|
|
252
|
+
const rawScale = accessibility.fontScale > 0 ? accessibility.fontScale : 1;
|
|
253
|
+
const maxScale =
|
|
254
|
+
typeof props.maxFontSizeMultiplier === 'number' &&
|
|
255
|
+
Number.isFinite(props.maxFontSizeMultiplier) &&
|
|
256
|
+
props.maxFontSizeMultiplier > 0
|
|
257
|
+
? props.maxFontSizeMultiplier
|
|
258
|
+
: Number.POSITIVE_INFINITY;
|
|
259
|
+
const fontScale = Math.min(rawScale, maxScale);
|
|
260
|
+
const minimumFontSize =
|
|
261
|
+
typeof props.minimumFontSize === 'number' &&
|
|
262
|
+
Number.isFinite(props.minimumFontSize) &&
|
|
263
|
+
props.minimumFontSize > 0
|
|
264
|
+
? props.minimumFontSize
|
|
265
|
+
: undefined;
|
|
266
|
+
const scaledFontSize =
|
|
267
|
+
minimumFontSize == null
|
|
268
|
+
? style.fontSize * fontScale
|
|
269
|
+
: Math.max(style.fontSize * fontScale, minimumFontSize);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
...style,
|
|
273
|
+
// Accessibility preferences affect layout, so text scaling is resolved
|
|
274
|
+
// before protocol encoding and Taffy measurement rather than as a paint-only
|
|
275
|
+
// native tweak.
|
|
276
|
+
fontSize: scaledFontSize,
|
|
277
|
+
lineHeight:
|
|
278
|
+
typeof style.lineHeight === 'number' && Number.isFinite(style.lineHeight) && style.lineHeight > 0
|
|
279
|
+
? style.lineHeight * fontScale
|
|
280
|
+
: style.lineHeight,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isPercentMinHeight(value: CanonicalStyle['minHeight']): boolean {
|
|
285
|
+
return (
|
|
286
|
+
typeof value === 'object' &&
|
|
287
|
+
value !== null &&
|
|
288
|
+
'type' in value &&
|
|
289
|
+
value.type === 'percent' &&
|
|
290
|
+
value.value === 100
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function applyRootScrollViewportDefaults(
|
|
295
|
+
instance: ElementNode,
|
|
296
|
+
style: CanonicalStyle,
|
|
297
|
+
): CanonicalStyle {
|
|
298
|
+
if (instance.tagType !== 'scroll' || instance.parent?.kind !== NodeKind.Root) {
|
|
299
|
+
return style;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (style.height !== undefined) {
|
|
303
|
+
return style;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const next: CanonicalStyle = { ...style };
|
|
307
|
+
|
|
308
|
+
// Percent min-height makes a root scroll grow with its content instead of
|
|
309
|
+
// staying viewport-bound; flex children need minHeight 0 to shrink and scroll.
|
|
310
|
+
if (isPercentMinHeight(next.minHeight)) {
|
|
311
|
+
delete next.minHeight;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (next.flexGrow === undefined) {
|
|
315
|
+
next.flexGrow = 1;
|
|
316
|
+
next.flexShrink = 1;
|
|
317
|
+
next.flexBasis = { type: 'points', value: 0 };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return next;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildDefaultCanonicalStyle(
|
|
324
|
+
defaultFlexDirection: FlexDirection,
|
|
325
|
+
isRNStyle = false,
|
|
326
|
+
): CanonicalStyle {
|
|
327
|
+
const style: CanonicalStyle = defaultFlexDirection === 'row'
|
|
328
|
+
? {}
|
|
329
|
+
: { flexDirection: defaultFlexDirection };
|
|
330
|
+
|
|
331
|
+
if (isRNStyle) {
|
|
332
|
+
style.flexShrink = 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return style;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function safeAreaInsetsEqual(
|
|
339
|
+
left: SafeAreaPropagationState['regions']['container'],
|
|
340
|
+
right: SafeAreaPropagationState['regions']['container'],
|
|
341
|
+
): boolean {
|
|
342
|
+
return (
|
|
343
|
+
left.top === right.top &&
|
|
344
|
+
left.right === right.right &&
|
|
345
|
+
left.bottom === right.bottom &&
|
|
346
|
+
left.left === right.left
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function keyboardStateEqual(
|
|
351
|
+
left: SafeAreaPropagationState['keyboard'],
|
|
352
|
+
right: SafeAreaPropagationState['keyboard'],
|
|
353
|
+
): boolean {
|
|
354
|
+
return (
|
|
355
|
+
left.visible === right.visible &&
|
|
356
|
+
left.occlusion.x === right.occlusion.x &&
|
|
357
|
+
left.occlusion.y === right.occlusion.y &&
|
|
358
|
+
left.occlusion.width === right.occlusion.width &&
|
|
359
|
+
left.occlusion.height === right.occlusion.height &&
|
|
360
|
+
left.progress === right.progress &&
|
|
361
|
+
left.animating === right.animating &&
|
|
362
|
+
left.mode === right.mode &&
|
|
363
|
+
left.animationCurve === right.animationCurve &&
|
|
364
|
+
left.animationDurationMs === right.animationDurationMs &&
|
|
365
|
+
left.interactive === right.interactive &&
|
|
366
|
+
left.accessoryHeight === right.accessoryHeight &&
|
|
367
|
+
left.source === right.source
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function safeAreaPropagationEqual(
|
|
372
|
+
left: SafeAreaPropagationState | null,
|
|
373
|
+
right: SafeAreaPropagationState | null,
|
|
374
|
+
): boolean {
|
|
375
|
+
if (left === right) {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
if (!left || !right) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
return (
|
|
382
|
+
left.rootId === right.rootId &&
|
|
383
|
+
safeAreaInsetsEqual(left.regions.container, right.regions.container) &&
|
|
384
|
+
safeAreaInsetsEqual(left.regions.displayCutout, right.regions.displayCutout) &&
|
|
385
|
+
safeAreaInsetsEqual(left.regions.gestures, right.regions.gestures) &&
|
|
386
|
+
keyboardStateEqual(left.keyboard, right.keyboard)
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const LAYOUT_AFFECTING_STYLE_KEYS =
|
|
391
|
+
'|width|height|minWidth|minHeight|maxWidth|maxHeight|paddingTop|paddingRight|paddingBottom|paddingLeft|marginTop|marginRight|marginBottom|marginLeft|flexDirection|flexWrap|justifyContent|alignItems|alignSelf|flexGrow|flexShrink|flexBasis|rowGap|columnGap|positionType|top|right|bottom|left|display|overflow|aspectRatio|direction|writingMode|fontFamily|fontSize|fontWeight|fontStyle|fontVariantNumeric|lineHeight|letterSpacing|numberOfLines|ellipsizeMode|';
|
|
392
|
+
|
|
393
|
+
const TRANSPARENT_COLOR: NonNullable<CanonicalStyle['backgroundColor']> = Object.freeze({
|
|
394
|
+
r: 0,
|
|
395
|
+
g: 0,
|
|
396
|
+
b: 0,
|
|
397
|
+
a: 0,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
function canonicalStyleValuesEqual(left: unknown, right: unknown): boolean {
|
|
401
|
+
if (left === right) {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (
|
|
406
|
+
typeof left === 'object' &&
|
|
407
|
+
left !== null &&
|
|
408
|
+
typeof right === 'object' &&
|
|
409
|
+
right !== null
|
|
410
|
+
) {
|
|
411
|
+
const leftRecord = left as Record<string, unknown>;
|
|
412
|
+
const rightRecord = right as Record<string, unknown>;
|
|
413
|
+
|
|
414
|
+
if ('type' in leftRecord || 'type' in rightRecord) {
|
|
415
|
+
return leftRecord.type === rightRecord.type && leftRecord.value === rightRecord.value;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if ('r' in leftRecord || 'r' in rightRecord) {
|
|
419
|
+
return (
|
|
420
|
+
leftRecord.r === rightRecord.r &&
|
|
421
|
+
leftRecord.g === rightRecord.g &&
|
|
422
|
+
leftRecord.b === rightRecord.b &&
|
|
423
|
+
leftRecord.a === rightRecord.a
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function collectChangedStyleKeys(
|
|
432
|
+
previousStyle: CanonicalStyle,
|
|
433
|
+
nextStyle: CanonicalStyle,
|
|
434
|
+
): Array<keyof CanonicalStyle> {
|
|
435
|
+
if (previousStyle === nextStyle) {
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const changed: Array<keyof CanonicalStyle> = [];
|
|
440
|
+
|
|
441
|
+
for (const key of Object.keys(previousStyle) as Array<keyof CanonicalStyle>) {
|
|
442
|
+
if (!canonicalStyleValuesEqual(previousStyle[key], nextStyle[key])) {
|
|
443
|
+
changed.push(key);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for (const key of Object.keys(nextStyle) as Array<keyof CanonicalStyle>) {
|
|
448
|
+
if (
|
|
449
|
+
!(key in previousStyle) &&
|
|
450
|
+
!canonicalStyleValuesEqual(previousStyle[key], nextStyle[key])
|
|
451
|
+
) {
|
|
452
|
+
changed.push(key);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return changed;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function styleKeyListIncludes(list: string, key: keyof CanonicalStyle): boolean {
|
|
460
|
+
return list.includes(`|${key}|`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function isTransformPatchStyleKey(key: keyof CanonicalStyle): boolean {
|
|
464
|
+
return (
|
|
465
|
+
key === 'transformX' ||
|
|
466
|
+
key === 'transformY' ||
|
|
467
|
+
key === 'transformScale' ||
|
|
468
|
+
key === 'transformRotate'
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function isPaintPatchStyleKey(key: keyof CanonicalStyle): boolean {
|
|
473
|
+
return key === 'backgroundColor' || key === 'opacity';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function styleKeysAffectLayout(changedKeys: readonly (keyof CanonicalStyle)[]): boolean {
|
|
477
|
+
for (const key of changedKeys) {
|
|
478
|
+
if (styleKeyListIncludes(LAYOUT_AFFECTING_STYLE_KEYS, key)) {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function encodeStyleUpdate(
|
|
487
|
+
encoder: ProtocolEncoder,
|
|
488
|
+
nodeId: number,
|
|
489
|
+
changedKeys: readonly (keyof CanonicalStyle)[],
|
|
490
|
+
nextStyle: CanonicalStyle,
|
|
491
|
+
): void {
|
|
492
|
+
const canPatch = changedKeys.every(
|
|
493
|
+
(key) => isTransformPatchStyleKey(key) || isPaintPatchStyleKey(key),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
if (!canPatch) {
|
|
497
|
+
setStyle(encoder, nodeId, nextStyle);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const transformChanged = changedKeys.some(isTransformPatchStyleKey);
|
|
502
|
+
if (transformChanged) {
|
|
503
|
+
setTransform(
|
|
504
|
+
encoder,
|
|
505
|
+
nodeId,
|
|
506
|
+
nextStyle.transformX ?? 0,
|
|
507
|
+
nextStyle.transformY ?? 0,
|
|
508
|
+
nextStyle.transformScale ?? 1,
|
|
509
|
+
nextStyle.transformRotate ?? 0,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (changedKeys.includes('opacity')) {
|
|
514
|
+
setOpacity(encoder, nodeId, nextStyle.opacity ?? 1);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (changedKeys.includes('backgroundColor')) {
|
|
518
|
+
setBackgroundColor(encoder, nodeId, nextStyle.backgroundColor ?? TRANSPARENT_COLOR);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Merge native defaults, className styles, and explicit inline style into a
|
|
524
|
+
* single canonical style object.
|
|
525
|
+
*
|
|
526
|
+
* The ordering matters:
|
|
527
|
+
* - renderer defaults are the lowest-priority baseline
|
|
528
|
+
* - `className` should be able to override those defaults
|
|
529
|
+
* - inline `style` must still beat `className`
|
|
530
|
+
*
|
|
531
|
+
* To preserve that ordering, inline styles are normalized without injecting the
|
|
532
|
+
* renderer defaults. We add the defaults as a separate first layer instead.
|
|
533
|
+
*/
|
|
534
|
+
export function resolveCanonicalStyleFromProps(
|
|
535
|
+
props: Record<string, unknown>,
|
|
536
|
+
defaultFlexDirection: FlexDirection,
|
|
537
|
+
classNameContext?: ClassNameResolutionContext,
|
|
538
|
+
isRNStyle = false,
|
|
539
|
+
): CanonicalStyle {
|
|
540
|
+
return {
|
|
541
|
+
...buildDefaultCanonicalStyle(defaultFlexDirection, isRNStyle),
|
|
542
|
+
...resolveClassNameStyle(props.className, classNameContext ?? { props }),
|
|
543
|
+
...normalizeStyle(props.style as FullStyle | undefined, 'row'),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// =============================================================================
|
|
548
|
+
// Protocol Encoder Instances (Multi-Root)
|
|
549
|
+
// =============================================================================
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Per-root protocol encoders.
|
|
553
|
+
* Each root gets its own encoder that targets a specific root_id.
|
|
554
|
+
*/
|
|
555
|
+
const encoders = new Map<number, ProtocolEncoder>();
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Claim a root ID for a public adapter surface.
|
|
559
|
+
*
|
|
560
|
+
* Multiple roots can be owned by the same adapter name, but a single root ID
|
|
561
|
+
* cannot be shared across adapters or handles at the same time.
|
|
562
|
+
*/
|
|
563
|
+
export function claimRoot(rootId: number, owner: string): void {
|
|
564
|
+
const existingOwner = rootOwners.get(rootId);
|
|
565
|
+
if (existingOwner && existingOwner !== owner) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
`Root ${rootId} is already in use by ${existingOwner}; ${owner} cannot claim it.`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
rootOwners.set(rootId, owner);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Release a previously claimed root ID.
|
|
576
|
+
*
|
|
577
|
+
* The owner check is intentionally strict so one adapter cannot accidentally
|
|
578
|
+
* release another adapter's root claim during teardown.
|
|
579
|
+
*/
|
|
580
|
+
export function releaseRoot(rootId: number, owner: string): void {
|
|
581
|
+
const existingOwner = rootOwners.get(rootId);
|
|
582
|
+
if (!existingOwner) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (existingOwner !== owner) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`Root ${rootId} is owned by ${existingOwner}; ${owner} cannot release it.`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
rootOwners.delete(rootId);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Read the current root owner for diagnostics and tests.
|
|
597
|
+
*/
|
|
598
|
+
export function getRootOwner(rootId: number): string | undefined {
|
|
599
|
+
return rootOwners.get(rootId);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Get or create the encoder for a given root ID.
|
|
604
|
+
*/
|
|
605
|
+
export function getEncoderForRoot(rootId: number): ProtocolEncoder {
|
|
606
|
+
let enc = encoders.get(rootId);
|
|
607
|
+
if (!enc) {
|
|
608
|
+
enc = createProtocolEncoder(1024 * 1024, rootId);
|
|
609
|
+
encoders.set(rootId, enc);
|
|
610
|
+
}
|
|
611
|
+
return enc;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Get the default encoder (root 0) for backwards compatibility.
|
|
616
|
+
*/
|
|
617
|
+
export function getEncoder(): ProtocolEncoder {
|
|
618
|
+
return getEncoderForRoot(0);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Get the shared encoder instance.
|
|
623
|
+
* @internal For testing only.
|
|
624
|
+
* @deprecated Use getEncoder() instead
|
|
625
|
+
*/
|
|
626
|
+
export function _getEncoder(): ProtocolEncoder {
|
|
627
|
+
return getEncoderForRoot(0);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Get the rootId for a node by walking up to the root.
|
|
632
|
+
* Falls back to 0 if no root is found.
|
|
633
|
+
*/
|
|
634
|
+
function getRootIdForNode(node: ElementNode | TextNode | RootNode): number {
|
|
635
|
+
let current: ElementNode | TextNode | RootNode | null = node;
|
|
636
|
+
while (current) {
|
|
637
|
+
if (current.kind === NodeKind.Root) {
|
|
638
|
+
return (current as RootNode).rootId;
|
|
639
|
+
}
|
|
640
|
+
current = current.parent as ElementNode | TextNode | RootNode | null;
|
|
641
|
+
}
|
|
642
|
+
return 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Walk up to the root container for a node.
|
|
647
|
+
*
|
|
648
|
+
* The DOM shim and adapter tests need the actual root object, not just the
|
|
649
|
+
* numeric root ID, so host-ops exposes the full lookup helper as part of the
|
|
650
|
+
* shared adapter contract.
|
|
651
|
+
*/
|
|
652
|
+
export function getRootContainerForNode(
|
|
653
|
+
node: ElementNode | TextNode | RootNode | null | undefined,
|
|
654
|
+
): RootNode | null {
|
|
655
|
+
let current = node;
|
|
656
|
+
while (current) {
|
|
657
|
+
if (current.kind === NodeKind.Root) {
|
|
658
|
+
return current;
|
|
659
|
+
}
|
|
660
|
+
current = current.parent as ElementNode | TextNode | RootNode | null;
|
|
661
|
+
}
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Get the encoder for a node based on its root.
|
|
667
|
+
*/
|
|
668
|
+
function getEncoderForNode(node: ElementNode | TextNode | RootNode): ProtocolEncoder {
|
|
669
|
+
return getEncoderForRoot(getRootIdForNode(node));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function getPendingCommitState(rootId: number): PendingCommitState {
|
|
673
|
+
let state = pendingCommitStateByRoot.get(rootId);
|
|
674
|
+
if (!state) {
|
|
675
|
+
state = {
|
|
676
|
+
layoutDirty: false,
|
|
677
|
+
warningAnalysisDirty: false,
|
|
678
|
+
dirtyChildren: new Set<RootNode | ElementNode>(),
|
|
679
|
+
changedViews: new Set<number>(),
|
|
680
|
+
};
|
|
681
|
+
pendingCommitStateByRoot.set(rootId, state);
|
|
682
|
+
}
|
|
683
|
+
return state;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function markLayoutDirtyForRoot(rootId: number): void {
|
|
687
|
+
getPendingCommitState(rootId).layoutDirty = true;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function markLayoutDirtyForNode(node: ElementNode | TextNode | RootNode): void {
|
|
691
|
+
markLayoutDirtyForRoot(getRootIdForNode(node));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function markViewChanged(node: ElementNode | TextNode | RootNode): void {
|
|
695
|
+
getPendingCommitState(getRootIdForNode(node)).changedViews.add(node.id);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function markChildrenDirty(parent: RootNode | ElementNode): void {
|
|
699
|
+
const state = getPendingCommitState(getRootIdForNode(parent));
|
|
700
|
+
state.dirtyChildren.add(parent);
|
|
701
|
+
state.layoutDirty = true;
|
|
702
|
+
state.warningAnalysisDirty = true;
|
|
703
|
+
state.changedViews.add(parent.id);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function flushDirtyChildren(
|
|
707
|
+
encoder: ProtocolEncoder,
|
|
708
|
+
state: PendingCommitState | undefined,
|
|
709
|
+
): void {
|
|
710
|
+
if (!state || state.dirtyChildren.size === 0) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
for (const node of state.dirtyChildren) {
|
|
715
|
+
if (node.kind !== NodeKind.Root && node.parent === null) {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
encodeChildren(encoder, node);
|
|
719
|
+
node.dirtyFlags = (node.dirtyFlags & ~DirtyFlags.Children) as DirtyFlags;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
state.dirtyChildren.clear();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function ensureLocaleRootSync(): void {
|
|
726
|
+
if (localeChangeUnsubscribe) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
localeChangeUnsubscribe = subscribeExactLocaleChanges(syncLiveRootInheritedState);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function ensureAccessibilityRootSync(): void {
|
|
734
|
+
if (accessibilityChangeUnsubscribe) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
accessibilityChangeUnsubscribe = subscribeExactAccessibilityChanges(syncLiveRootInheritedState);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function maybeDisposeLocaleRootSync(): void {
|
|
742
|
+
if (liveRoots.size > 0 || !localeChangeUnsubscribe) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
localeChangeUnsubscribe();
|
|
747
|
+
localeChangeUnsubscribe = null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function maybeDisposeAccessibilityRootSync(): void {
|
|
751
|
+
if (liveRoots.size > 0 || !accessibilityChangeUnsubscribe) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
accessibilityChangeUnsubscribe();
|
|
756
|
+
accessibilityChangeUnsubscribe = null;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// =============================================================================
|
|
760
|
+
// Event Handler Registry
|
|
761
|
+
// =============================================================================
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Global event handler registry.
|
|
765
|
+
* Maps handler IDs to handler functions.
|
|
766
|
+
* Shared across all frameworks (React, Solid, Vue).
|
|
767
|
+
*/
|
|
768
|
+
interface HostEventRegistry {
|
|
769
|
+
handlers: Map<number, Function>;
|
|
770
|
+
nextHandlerId: number;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const HOST_EVENT_REGISTRY_KEY = '__exactHostEventRegistry';
|
|
774
|
+
|
|
775
|
+
function getHostEventRegistry(): HostEventRegistry {
|
|
776
|
+
const scope = globalThis as Record<string, unknown>;
|
|
777
|
+
const existing = scope[HOST_EVENT_REGISTRY_KEY];
|
|
778
|
+
if (
|
|
779
|
+
existing &&
|
|
780
|
+
typeof existing === 'object' &&
|
|
781
|
+
(existing as Partial<HostEventRegistry>).handlers instanceof Map &&
|
|
782
|
+
typeof (existing as Partial<HostEventRegistry>).nextHandlerId === 'number'
|
|
783
|
+
) {
|
|
784
|
+
return existing as HostEventRegistry;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const registry: HostEventRegistry = {
|
|
788
|
+
handlers: new Map<number, Function>(),
|
|
789
|
+
nextHandlerId: 1,
|
|
790
|
+
};
|
|
791
|
+
scope[HOST_EVENT_REGISTRY_KEY] = registry;
|
|
792
|
+
return registry;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const eventRegistry = getHostEventRegistry();
|
|
796
|
+
const eventHandlers = eventRegistry.handlers;
|
|
797
|
+
type NativeEventBatcher = (callback: () => void) => void;
|
|
798
|
+
let nativeEventBatcher: NativeEventBatcher | null = null;
|
|
799
|
+
|
|
800
|
+
export function setNativeEventBatcher(batcher: NativeEventBatcher | null): void {
|
|
801
|
+
nativeEventBatcher = batcher;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Register an event handler and return its ID.
|
|
807
|
+
*/
|
|
808
|
+
export function registerHandler(fn: Function): number {
|
|
809
|
+
const id = eventRegistry.nextHandlerId++;
|
|
810
|
+
eventHandlers.set(id, fn);
|
|
811
|
+
return id;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Unregister an event handler by ID and notify native.
|
|
816
|
+
*
|
|
817
|
+
* @param id - The handler ID to unregister
|
|
818
|
+
* @param nodeId - The node ID to unbind from (optional, for native unbinding)
|
|
819
|
+
* @param eventType - The event type to unbind (optional, for native unbinding)
|
|
820
|
+
* @param rootId - The owning root for the node/event pair. Multi-root adapters
|
|
821
|
+
* must supply the real root so unbind ops land on the correct encoder.
|
|
822
|
+
*/
|
|
823
|
+
export function unregisterHandler(
|
|
824
|
+
id: number,
|
|
825
|
+
nodeId?: number,
|
|
826
|
+
eventType?: EventType,
|
|
827
|
+
rootId: number = 0,
|
|
828
|
+
): void {
|
|
829
|
+
eventHandlers.delete(id);
|
|
830
|
+
|
|
831
|
+
// Send unbind to native if we have the node and event info
|
|
832
|
+
if (nodeId !== undefined && eventType !== undefined) {
|
|
833
|
+
getEncoderForRoot(rootId).unbindEvent(nodeId, eventType);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Get an event handler by ID.
|
|
839
|
+
*/
|
|
840
|
+
export function getHandler(id: number): Function | undefined {
|
|
841
|
+
return eventHandlers.get(id);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Update an existing handler's function without changing its ID.
|
|
846
|
+
* Used when a framework updates a callback prop.
|
|
847
|
+
*/
|
|
848
|
+
export function updateHandler(id: number, fn: Function): void {
|
|
849
|
+
if (eventHandlers.has(id)) {
|
|
850
|
+
eventHandlers.set(id, fn);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Clear all registered handlers.
|
|
856
|
+
* @internal For testing only.
|
|
857
|
+
*/
|
|
858
|
+
export function _clearHandlers(): void {
|
|
859
|
+
eventHandlers.clear();
|
|
860
|
+
eventRegistry.nextHandlerId = 1;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// =============================================================================
|
|
864
|
+
// Event Dispatch (called from native)
|
|
865
|
+
// =============================================================================
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Handle an event dispatched from native.
|
|
869
|
+
* Looks up the handler and calls it with the payload.
|
|
870
|
+
* Framework reactivity and the renderer commit lifecycle handle any
|
|
871
|
+
* subsequent updates.
|
|
872
|
+
*/
|
|
873
|
+
function handleNativeEvent(handlerId: number, payload: unknown): void {
|
|
874
|
+
const handler = getHandler(handlerId);
|
|
875
|
+
if (!handler) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
880
|
+
recordInboundProtocolEvent(handlerId, payload);
|
|
881
|
+
}
|
|
882
|
+
invokeHandler(handlerId, handler, payload);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function invokeHandler(handlerId: number, handler: Function, payload: unknown): void {
|
|
886
|
+
const invoke = (): void => {
|
|
887
|
+
try {
|
|
888
|
+
handler(payload);
|
|
889
|
+
} catch (error) {
|
|
890
|
+
console.error(`[HostOps] Error in event handler ${handlerId}:`, error);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
if (nativeEventBatcher) {
|
|
895
|
+
try {
|
|
896
|
+
nativeEventBatcher(invoke);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
console.error(`[HostOps] Error in native event batcher for handler ${handlerId}:`, error);
|
|
899
|
+
}
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
invoke();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export function dispatchSyntheticEvent(
|
|
907
|
+
instance: ElementNode,
|
|
908
|
+
eventType: EventType,
|
|
909
|
+
payload: unknown,
|
|
910
|
+
): boolean {
|
|
911
|
+
const binding = instance.events.get(eventType);
|
|
912
|
+
if (!binding) {
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
invokeHandler(binding.handlerId, binding.handler, payload);
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Install the global event handler (shared across all frameworks)
|
|
921
|
+
// Always overwrite to ensure we have the correct handler
|
|
922
|
+
(globalThis as Record<string, unknown>).__exactDispatchEvent = handleNativeEvent;
|
|
923
|
+
|
|
924
|
+
// =============================================================================
|
|
925
|
+
// Node Operations
|
|
926
|
+
// =============================================================================
|
|
927
|
+
|
|
928
|
+
// Re-export node operations for convenience
|
|
929
|
+
export {
|
|
930
|
+
NodeKind,
|
|
931
|
+
createElementNode,
|
|
932
|
+
createTextNode,
|
|
933
|
+
createRootNode,
|
|
934
|
+
nodeAppendChild,
|
|
935
|
+
nodeInsertBefore,
|
|
936
|
+
nodeRemoveChild,
|
|
937
|
+
nodeClearChildren,
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
// Re-export types
|
|
941
|
+
export type { ElementNode, TextNode, RootNode };
|
|
942
|
+
|
|
943
|
+
// =============================================================================
|
|
944
|
+
// Tag Configuration
|
|
945
|
+
// =============================================================================
|
|
946
|
+
|
|
947
|
+
export { getTagConfig, defaultTagConfig };
|
|
948
|
+
export type { TagConfig };
|
|
949
|
+
|
|
950
|
+
// =============================================================================
|
|
951
|
+
// Style and Props Utilities
|
|
952
|
+
// =============================================================================
|
|
953
|
+
|
|
954
|
+
export { normalizeStyle, normalizeTransition, stylesEqual, transitionsEqual };
|
|
955
|
+
export { normalizeProps, propsEqual };
|
|
956
|
+
|
|
957
|
+
// =============================================================================
|
|
958
|
+
// Protocol Encoding
|
|
959
|
+
// =============================================================================
|
|
960
|
+
|
|
961
|
+
export {
|
|
962
|
+
encodeCreateElement,
|
|
963
|
+
encodeCreateText,
|
|
964
|
+
encodeProps,
|
|
965
|
+
encodeEvents,
|
|
966
|
+
encodeChildren,
|
|
967
|
+
encodeDestroy,
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
export { EventType, NodeType };
|
|
971
|
+
|
|
972
|
+
// =============================================================================
|
|
973
|
+
// Event Processing Helper
|
|
974
|
+
// =============================================================================
|
|
975
|
+
|
|
976
|
+
type Props = Record<string, unknown>;
|
|
977
|
+
|
|
978
|
+
const PRESS_EVENT_BINDINGS = [
|
|
979
|
+
{ eventType: EventType.Press, propName: 'onPress' },
|
|
980
|
+
{ eventType: EventType.PressIn, propName: 'onPressIn' },
|
|
981
|
+
{ eventType: EventType.PressOut, propName: 'onPressOut' },
|
|
982
|
+
{ eventType: EventType.LongPress, propName: 'onLongPress' },
|
|
983
|
+
] as const;
|
|
984
|
+
|
|
985
|
+
const FOCUS_AND_KEYBOARD_BINDINGS = [
|
|
986
|
+
{ eventType: EventType.Focus, propName: 'onFocus', normalize: normalizeFocusChangePayload },
|
|
987
|
+
{ eventType: EventType.Blur, propName: 'onBlur', normalize: normalizeFocusChangePayload },
|
|
988
|
+
{ eventType: EventType.KeyDown, propName: 'onKeyDown', normalize: normalizeKeyPayload },
|
|
989
|
+
{ eventType: EventType.KeyUp, propName: 'onKeyUp', normalize: normalizeKeyPayload },
|
|
990
|
+
{ eventType: EventType.FocusIn, propName: 'onFocusIn', normalize: normalizeFocusChangePayload },
|
|
991
|
+
{ eventType: EventType.FocusOut, propName: 'onFocusOut', normalize: normalizeFocusChangePayload },
|
|
992
|
+
{
|
|
993
|
+
eventType: EventType.AccessibilityAction,
|
|
994
|
+
propName: 'onAccessibilityAction',
|
|
995
|
+
normalize: normalizeAccessibilityActionPayload,
|
|
996
|
+
},
|
|
997
|
+
] as const;
|
|
998
|
+
|
|
999
|
+
const EVENT_PROP_NAMES = [
|
|
1000
|
+
'onPress',
|
|
1001
|
+
'onPressIn',
|
|
1002
|
+
'onPressOut',
|
|
1003
|
+
'onLongPress',
|
|
1004
|
+
'onValueChange',
|
|
1005
|
+
'onChangeText',
|
|
1006
|
+
'onChange',
|
|
1007
|
+
'onSelectionChange',
|
|
1008
|
+
'onFocus',
|
|
1009
|
+
'onBlur',
|
|
1010
|
+
'onKeyDown',
|
|
1011
|
+
'onKeyUp',
|
|
1012
|
+
'onSubmitEditing',
|
|
1013
|
+
'onFocusIn',
|
|
1014
|
+
'onFocusOut',
|
|
1015
|
+
'onAccessibilityAction',
|
|
1016
|
+
'onTransitionEnd',
|
|
1017
|
+
'onScroll',
|
|
1018
|
+
'scrollEventThrottle',
|
|
1019
|
+
'onLoadStart',
|
|
1020
|
+
'onLoad',
|
|
1021
|
+
'onError',
|
|
1022
|
+
'onDisplay',
|
|
1023
|
+
] as const;
|
|
1024
|
+
|
|
1025
|
+
function parseRequestedDirection(value: unknown): Direction | undefined {
|
|
1026
|
+
return value === 'ltr' || value === 'rtl' || value === 'auto'
|
|
1027
|
+
? value
|
|
1028
|
+
: undefined;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function parseRequestedWritingMode(value: unknown): WritingMode | undefined {
|
|
1032
|
+
return value === 'horizontal-tb' || value === 'vertical-rl' || value === 'vertical-lr'
|
|
1033
|
+
? value
|
|
1034
|
+
: undefined;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function getRootInheritedState(rootId: number = 0): InheritedElementState {
|
|
1038
|
+
const locale = getExactLocaleSnapshot();
|
|
1039
|
+
return {
|
|
1040
|
+
rootId,
|
|
1041
|
+
direction: locale.direction,
|
|
1042
|
+
writingMode: 'horizontal-tb',
|
|
1043
|
+
lang: locale.tag,
|
|
1044
|
+
safeArea: createRootSafeAreaPropagation(rootId),
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function getInheritedState(
|
|
1049
|
+
parent: ElementNode | RootNode | null | undefined,
|
|
1050
|
+
): InheritedElementState {
|
|
1051
|
+
if (parent?.kind === NodeKind.Element) {
|
|
1052
|
+
return {
|
|
1053
|
+
rootId: parent.propagatedSafeArea?.rootId ?? getRootIdForNode(parent),
|
|
1054
|
+
direction: parent.resolvedDirection,
|
|
1055
|
+
writingMode: parent.resolvedWritingMode,
|
|
1056
|
+
lang: parent.resolvedLang,
|
|
1057
|
+
safeArea: parent.propagatedSafeArea ?? createRootSafeAreaPropagation(getRootIdForNode(parent)),
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return getRootInheritedState(parent?.kind === NodeKind.Root ? parent.rootId : 0);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function readInlineStyleValue(props: Props, key: string): unknown {
|
|
1065
|
+
const style = props.style;
|
|
1066
|
+
if (!style || typeof style !== 'object') {
|
|
1067
|
+
return undefined;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return (style as Record<string, unknown>)[key];
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function getRequestedDirection(
|
|
1074
|
+
props: Props,
|
|
1075
|
+
classNameStyle: CanonicalStyle,
|
|
1076
|
+
): Direction | undefined {
|
|
1077
|
+
return parseRequestedDirection(readInlineStyleValue(props, 'direction')) ?? classNameStyle.direction;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function getRequestedWritingMode(
|
|
1081
|
+
props: Props,
|
|
1082
|
+
classNameStyle: CanonicalStyle,
|
|
1083
|
+
): WritingMode | undefined {
|
|
1084
|
+
return parseRequestedWritingMode(readInlineStyleValue(props, 'writingMode')) ?? classNameStyle.writingMode;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function getAutoDirectionText(props: CanonicalProps): string | undefined {
|
|
1088
|
+
return props.textContent ?? props.value;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function hasStableTextLayoutHint(props: Props): boolean {
|
|
1092
|
+
return props.__exactTextLayoutStable === true;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function hasFixedPointTextLayout(style: CanonicalStyle): boolean {
|
|
1096
|
+
return (
|
|
1097
|
+
style.width?.type === 'points' &&
|
|
1098
|
+
isFinite(style.width.value) &&
|
|
1099
|
+
style.height?.type === 'points' &&
|
|
1100
|
+
isFinite(style.height.value)
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function canPatchElementTextWithoutLayout(instance: ElementNode, props: Props): boolean {
|
|
1105
|
+
return (
|
|
1106
|
+
instance.tagType === 'text' &&
|
|
1107
|
+
hasStableTextLayoutHint(props) &&
|
|
1108
|
+
hasFixedPointTextLayout(instance.style)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Style fields the kernel reads when measuring a raw text leaf (the
|
|
1114
|
+
* TextMeasureCallback in kernel ffi.rs). A raw text leaf is a separate
|
|
1115
|
+
* kernel node from its styled parent <Text> element and the kernel does not
|
|
1116
|
+
* inherit styles across nodes, so this subset must be encoded onto the leaf
|
|
1117
|
+
* explicitly or the leaf measures with the default font while the host
|
|
1118
|
+
* renders the parent's — adapters that mount text as child leaves (Contract,
|
|
1119
|
+
* Solid) get boxes sized for the wrong font.
|
|
1120
|
+
*/
|
|
1121
|
+
const TEXT_LEAF_MEASURE_STYLE_KEYS = [
|
|
1122
|
+
'fontSize',
|
|
1123
|
+
'fontWeight',
|
|
1124
|
+
'fontStyle',
|
|
1125
|
+
'fontFamily',
|
|
1126
|
+
'lineHeight',
|
|
1127
|
+
'letterSpacing',
|
|
1128
|
+
'fontVariantNumeric',
|
|
1129
|
+
'direction',
|
|
1130
|
+
'numberOfLines',
|
|
1131
|
+
] as const satisfies readonly (keyof CanonicalStyle)[];
|
|
1132
|
+
|
|
1133
|
+
function textMeasureStyleSubset(style: CanonicalStyle): CanonicalStyle | null {
|
|
1134
|
+
let subset: Record<string, unknown> | null = null;
|
|
1135
|
+
for (const key of TEXT_LEAF_MEASURE_STYLE_KEYS) {
|
|
1136
|
+
const value = style[key];
|
|
1137
|
+
if (value !== undefined) {
|
|
1138
|
+
(subset ??= {})[key] = value;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return subset as CanonicalStyle | null;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function mergeTextMeasureStyles(
|
|
1145
|
+
inheritedStyle: CanonicalStyle | null,
|
|
1146
|
+
ownStyle: CanonicalStyle | null,
|
|
1147
|
+
): CanonicalStyle | null {
|
|
1148
|
+
if (!inheritedStyle) {
|
|
1149
|
+
return ownStyle;
|
|
1150
|
+
}
|
|
1151
|
+
if (!ownStyle) {
|
|
1152
|
+
return inheritedStyle;
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
...inheritedStyle,
|
|
1156
|
+
...ownStyle,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function inheritedTextMeasureStyleForNode(node: ElementNode): CanonicalStyle | null {
|
|
1161
|
+
const ancestors: ElementNode[] = [];
|
|
1162
|
+
let cursor: ElementNode | RootNode | TextNode | null = node.parent as
|
|
1163
|
+
| ElementNode
|
|
1164
|
+
| RootNode
|
|
1165
|
+
| TextNode
|
|
1166
|
+
| null;
|
|
1167
|
+
while (cursor?.kind === NodeKind.Element) {
|
|
1168
|
+
const ancestor = cursor as ElementNode;
|
|
1169
|
+
ancestors.unshift(ancestor);
|
|
1170
|
+
cursor = ancestor.parent as ElementNode | RootNode | TextNode | null;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
let inheritedStyle: CanonicalStyle | null = null;
|
|
1174
|
+
let inInlineTextContext = false;
|
|
1175
|
+
for (const ancestor of ancestors) {
|
|
1176
|
+
if (ancestor.tagType === 'text') {
|
|
1177
|
+
inheritedStyle = mergeTextMeasureStyles(
|
|
1178
|
+
inheritedStyle,
|
|
1179
|
+
textMeasureStyleSubset(ancestor.style),
|
|
1180
|
+
);
|
|
1181
|
+
inInlineTextContext = true;
|
|
1182
|
+
} else if (inInlineTextContext && ancestor.tagType === 'pressable') {
|
|
1183
|
+
continue;
|
|
1184
|
+
} else {
|
|
1185
|
+
inheritedStyle = null;
|
|
1186
|
+
inInlineTextContext = false;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
return inheritedStyle;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function effectiveTextMeasureStyleForNode(
|
|
1194
|
+
node: ElementNode,
|
|
1195
|
+
inheritedStyle: CanonicalStyle | null,
|
|
1196
|
+
): CanonicalStyle | null {
|
|
1197
|
+
return mergeTextMeasureStyles(inheritedStyle, textMeasureStyleSubset(node.style));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function inheritedTextMeasureStyleForChild(
|
|
1201
|
+
child: ElementNode | TextNode,
|
|
1202
|
+
inheritedStyle: CanonicalStyle | null,
|
|
1203
|
+
): CanonicalStyle | null {
|
|
1204
|
+
if (child.kind !== NodeKind.Element) {
|
|
1205
|
+
return inheritedStyle;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (child.tagType === 'text') {
|
|
1209
|
+
return effectiveTextMeasureStyleForNode(child, inheritedStyle);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
return inheritedStyle && child.tagType === 'pressable' ? inheritedStyle : null;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function inheritedTextMeasureStyleForInsertedChildren(
|
|
1216
|
+
parent: ElementNode | RootNode,
|
|
1217
|
+
): CanonicalStyle | null {
|
|
1218
|
+
if (parent.kind !== NodeKind.Element) {
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const inheritedStyle = inheritedTextMeasureStyleForNode(parent);
|
|
1223
|
+
if (parent.tagType === 'text') {
|
|
1224
|
+
return effectiveTextMeasureStyleForNode(parent, inheritedStyle);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
return inheritedStyle && parent.tagType === 'pressable' ? inheritedStyle : null;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function styleKeysAffectTextLeafMeasure(
|
|
1231
|
+
changedKeys: readonly (keyof CanonicalStyle)[],
|
|
1232
|
+
): boolean {
|
|
1233
|
+
return changedKeys.some((key) =>
|
|
1234
|
+
(TEXT_LEAF_MEASURE_STYLE_KEYS as readonly string[]).includes(key),
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/** Re-encode the measure-style subset onto already-created text leaf children. */
|
|
1239
|
+
function encodeTextLeafMeasureStyles(encoder: ProtocolEncoder, parent: ElementNode): void {
|
|
1240
|
+
const inheritedStyle = inheritedTextMeasureStyleForNode(parent);
|
|
1241
|
+
const parentMeasureStyle = effectiveTextMeasureStyleForNode(parent, inheritedStyle);
|
|
1242
|
+
if (!parentMeasureStyle) {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (inheritedStyle) {
|
|
1247
|
+
setStyle(encoder, parent.id, parentMeasureStyle);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
for (const child of parent.children) {
|
|
1251
|
+
encodeInheritedTextMeasureStyles(encoder, child, parentMeasureStyle);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function encodeInheritedTextMeasureStyles(
|
|
1256
|
+
encoder: ProtocolEncoder,
|
|
1257
|
+
node: ElementNode | TextNode,
|
|
1258
|
+
inheritedStyle: CanonicalStyle | null,
|
|
1259
|
+
): void {
|
|
1260
|
+
if (!inheritedStyle || hasDirtyFlag(node, DirtyFlags.Created)) {
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (node.kind === NodeKind.Text) {
|
|
1265
|
+
setStyle(encoder, node.id, inheritedStyle);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const nodeMeasureStyle = inheritedTextMeasureStyleForChild(node, inheritedStyle);
|
|
1270
|
+
if (!nodeMeasureStyle) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (node.tagType === 'text') {
|
|
1275
|
+
setStyle(encoder, node.id, nodeMeasureStyle);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
for (const child of node.children) {
|
|
1279
|
+
encodeInheritedTextMeasureStyles(encoder, child, nodeMeasureStyle);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function canPatchTextNodeWithoutLayout(instance: TextNode): boolean {
|
|
1284
|
+
const parent = instance.parent;
|
|
1285
|
+
if (parent?.kind !== NodeKind.Element) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
const elementParent = parent as ElementNode;
|
|
1289
|
+
|
|
1290
|
+
return (
|
|
1291
|
+
elementParent.tagType === 'text' &&
|
|
1292
|
+
hasStableTextLayoutHint(elementParent.originalProps) &&
|
|
1293
|
+
hasFixedPointTextLayout(elementParent.style)
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function serializeNativeViewProps(
|
|
1298
|
+
config: TagConfig,
|
|
1299
|
+
props: Props,
|
|
1300
|
+
): string {
|
|
1301
|
+
const metadata = config.nativeView;
|
|
1302
|
+
if (!metadata) {
|
|
1303
|
+
return '{}';
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const payload: Record<string, unknown> = {};
|
|
1307
|
+
for (const key of metadata.propKeys) {
|
|
1308
|
+
if (props[key] !== undefined) {
|
|
1309
|
+
payload[key] = props[key];
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
try {
|
|
1314
|
+
return JSON.stringify(payload);
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
if (__DEV__) {
|
|
1317
|
+
console.warn(
|
|
1318
|
+
`[HostOps] Failed to serialize native-view props for ${metadata.moduleName}:`,
|
|
1319
|
+
error,
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
return '{}';
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function augmentCanonicalPropsForNativeView(
|
|
1327
|
+
config: TagConfig,
|
|
1328
|
+
props: Props,
|
|
1329
|
+
canonicalProps: CanonicalProps,
|
|
1330
|
+
): CanonicalProps {
|
|
1331
|
+
const metadata = config.nativeView;
|
|
1332
|
+
if (!metadata) {
|
|
1333
|
+
return canonicalProps;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return {
|
|
1337
|
+
...canonicalProps,
|
|
1338
|
+
nativeViewModuleName: metadata.moduleName,
|
|
1339
|
+
nativeViewProps: serializeNativeViewProps(config, props),
|
|
1340
|
+
nativeViewSelectionTier: metadata.selection?.tier,
|
|
1341
|
+
nativeViewGesturePolicy: metadata.selection?.gesturePolicy,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function resolveDirectionForElement(
|
|
1346
|
+
originalTag: string,
|
|
1347
|
+
tagType: CanonicalTagType,
|
|
1348
|
+
requestedDirection: Direction | undefined,
|
|
1349
|
+
props: CanonicalProps,
|
|
1350
|
+
inherited: InheritedElementState,
|
|
1351
|
+
): ResolvedDirection {
|
|
1352
|
+
if (requestedDirection === 'ltr' || requestedDirection === 'rtl') {
|
|
1353
|
+
return requestedDirection;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
if (requestedDirection !== 'auto') {
|
|
1357
|
+
return inherited.direction;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const text = getAutoDirectionText(props);
|
|
1361
|
+
if (text !== undefined) {
|
|
1362
|
+
return detectTextDirection(text, inherited.direction);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (__DEV__ && !warnedAutoDirectionTags.has(originalTag)) {
|
|
1366
|
+
warnedAutoDirectionTags.add(originalTag);
|
|
1367
|
+
console.warn(
|
|
1368
|
+
`[HostOps] direction="auto" requires text content; falling back to inherited direction for <${originalTag}> (${tagType}).`,
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return inherited.direction;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
let resolveElementStateCallCount = 0;
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Number of resolveElementState invocations since the last reset.
|
|
1379
|
+
* @internal For testing only (verifies the updateInstanceProps fast path).
|
|
1380
|
+
*/
|
|
1381
|
+
export function _getResolveElementStateCallCount(): number {
|
|
1382
|
+
return resolveElementStateCallCount;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/** @internal For testing only. */
|
|
1386
|
+
export function _resetResolveElementStateCallCount(): void {
|
|
1387
|
+
resolveElementStateCallCount = 0;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function resolveElementState(
|
|
1391
|
+
instance: ElementNode,
|
|
1392
|
+
config: TagConfig,
|
|
1393
|
+
originalTag: string,
|
|
1394
|
+
props: Props,
|
|
1395
|
+
inherited: InheritedElementState,
|
|
1396
|
+
): ResolvedElementState {
|
|
1397
|
+
resolveElementStateCallCount += 1;
|
|
1398
|
+
const normalizedCanonicalProps = augmentCanonicalPropsForNativeView(
|
|
1399
|
+
config,
|
|
1400
|
+
props,
|
|
1401
|
+
normalizeProps(config.canonicalType, props, config.isRNStyle),
|
|
1402
|
+
);
|
|
1403
|
+
const paragraph = config.canonicalType === 'text'
|
|
1404
|
+
? lowerTextPropsToParagraphSpec(props)
|
|
1405
|
+
: null;
|
|
1406
|
+
const canonicalProps =
|
|
1407
|
+
config.canonicalType === 'text'
|
|
1408
|
+
? {
|
|
1409
|
+
...normalizedCanonicalProps,
|
|
1410
|
+
textContent: paragraph?.plainText ?? normalizedCanonicalProps.textContent,
|
|
1411
|
+
}
|
|
1412
|
+
: normalizedCanonicalProps;
|
|
1413
|
+
const classNameStyle = resolveClassNameStyle(props.className, { props });
|
|
1414
|
+
const resolvedDirection = resolveDirectionForElement(
|
|
1415
|
+
originalTag,
|
|
1416
|
+
config.canonicalType,
|
|
1417
|
+
getRequestedDirection(props, classNameStyle),
|
|
1418
|
+
canonicalProps,
|
|
1419
|
+
inherited,
|
|
1420
|
+
);
|
|
1421
|
+
const resolvedWritingMode =
|
|
1422
|
+
getRequestedWritingMode(props, classNameStyle) ?? inherited.writingMode;
|
|
1423
|
+
const resolvedLang = canonicalProps.lang ?? inherited.lang;
|
|
1424
|
+
const contentContainerStyle = config.canonicalType === 'scroll'
|
|
1425
|
+
? normalizeStyle(props.contentContainerStyle as FullStyle | undefined, 'row', {
|
|
1426
|
+
resolvedDirection,
|
|
1427
|
+
resolvedWritingMode,
|
|
1428
|
+
includeInheritedValues: true,
|
|
1429
|
+
})
|
|
1430
|
+
: {};
|
|
1431
|
+
const inlineStyle = normalizeStyle(props.style as FullStyle | undefined, 'row', {
|
|
1432
|
+
resolvedDirection,
|
|
1433
|
+
resolvedWritingMode,
|
|
1434
|
+
includeInheritedValues: true,
|
|
1435
|
+
});
|
|
1436
|
+
const style = applyTextLayoutPropStyleOverrides(
|
|
1437
|
+
config.canonicalType,
|
|
1438
|
+
applyPropDrivenStyleOverrides(
|
|
1439
|
+
config.canonicalType,
|
|
1440
|
+
{
|
|
1441
|
+
...buildDefaultCanonicalStyle(config.defaultFlexDirection, config.isRNStyle),
|
|
1442
|
+
...classNameStyle,
|
|
1443
|
+
...contentContainerStyle,
|
|
1444
|
+
...inlineStyle,
|
|
1445
|
+
direction: resolvedDirection,
|
|
1446
|
+
writingMode: resolvedWritingMode,
|
|
1447
|
+
},
|
|
1448
|
+
canonicalProps,
|
|
1449
|
+
props,
|
|
1450
|
+
),
|
|
1451
|
+
props,
|
|
1452
|
+
);
|
|
1453
|
+
const accessibilityAwareStyle = applyRootScrollViewportDefaults(
|
|
1454
|
+
instance,
|
|
1455
|
+
applyAccessibilityDrivenStyleOverrides(style, canonicalProps),
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
const safeAreaResolution = resolveSafeAreaForElement(
|
|
1459
|
+
instance,
|
|
1460
|
+
instance.parent as ElementNode | RootNode | null,
|
|
1461
|
+
accessibilityAwareStyle,
|
|
1462
|
+
inherited.safeArea,
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
if (config.canonicalType === 'scroll' && accessibilityAwareStyle.overflow === undefined) {
|
|
1466
|
+
safeAreaResolution.style.overflow = 'scroll';
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
return {
|
|
1470
|
+
props: canonicalProps,
|
|
1471
|
+
paragraphSpec: paragraph?.spec ?? null,
|
|
1472
|
+
paragraphKey: paragraph?.key ?? null,
|
|
1473
|
+
style: safeAreaResolution.style,
|
|
1474
|
+
transitions: normalizeTransition((props.style as FullStyle | undefined)?.transition),
|
|
1475
|
+
resolvedDirection,
|
|
1476
|
+
resolvedWritingMode,
|
|
1477
|
+
resolvedLang,
|
|
1478
|
+
safeAreaState: safeAreaResolution.safeAreaState,
|
|
1479
|
+
propagatedSafeArea: safeAreaResolution.propagatedSafeArea,
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function applyResolvedElementState(
|
|
1484
|
+
instance: ElementNode,
|
|
1485
|
+
state: ResolvedElementState,
|
|
1486
|
+
): void {
|
|
1487
|
+
instance.props = state.props;
|
|
1488
|
+
instance.paragraphSpec = state.paragraphSpec;
|
|
1489
|
+
instance.paragraphKey = state.paragraphKey;
|
|
1490
|
+
instance.style = state.style;
|
|
1491
|
+
instance.transitions = state.transitions;
|
|
1492
|
+
instance.resolvedDirection = state.resolvedDirection;
|
|
1493
|
+
instance.resolvedWritingMode = state.resolvedWritingMode;
|
|
1494
|
+
instance.resolvedLang = state.resolvedLang;
|
|
1495
|
+
instance.safeAreaState = state.safeAreaState;
|
|
1496
|
+
instance.propagatedSafeArea = state.propagatedSafeArea;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function canRefreshCreatedMountEnvironment(
|
|
1500
|
+
instance: ElementNode,
|
|
1501
|
+
inherited: InheritedElementState,
|
|
1502
|
+
): boolean {
|
|
1503
|
+
if (instance.safeAreaState !== null) {
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const expectedLang = instance.props.lang ?? inherited.lang;
|
|
1508
|
+
return (
|
|
1509
|
+
instance.tagType !== 'text' &&
|
|
1510
|
+
instance.resolvedDirection === inherited.direction &&
|
|
1511
|
+
instance.resolvedWritingMode === inherited.writingMode &&
|
|
1512
|
+
instance.resolvedLang === expectedLang
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function refreshCreatedMountEnvironment(
|
|
1517
|
+
instance: ElementNode,
|
|
1518
|
+
inherited: InheritedElementState,
|
|
1519
|
+
): void {
|
|
1520
|
+
instance.resolvedLang = instance.props.lang ?? inherited.lang;
|
|
1521
|
+
|
|
1522
|
+
const safeAreaResolution = resolveSafeAreaForElement(
|
|
1523
|
+
instance,
|
|
1524
|
+
instance.parent as ElementNode | RootNode | null,
|
|
1525
|
+
applyRootScrollViewportDefaults(instance, instance.style),
|
|
1526
|
+
inherited.safeArea,
|
|
1527
|
+
);
|
|
1528
|
+
instance.style = safeAreaResolution.style;
|
|
1529
|
+
instance.safeAreaState = safeAreaResolution.safeAreaState;
|
|
1530
|
+
instance.propagatedSafeArea = safeAreaResolution.propagatedSafeArea;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
interface NormalizedTextChangePayload {
|
|
1534
|
+
nativeEvent: {
|
|
1535
|
+
text: string;
|
|
1536
|
+
selection?: {
|
|
1537
|
+
start: number;
|
|
1538
|
+
end: number;
|
|
1539
|
+
};
|
|
1540
|
+
isComposing?: boolean;
|
|
1541
|
+
compositionRange?: {
|
|
1542
|
+
start: number;
|
|
1543
|
+
end: number;
|
|
1544
|
+
};
|
|
1545
|
+
textRevision?: number;
|
|
1546
|
+
classification?: string;
|
|
1547
|
+
textChanged?: boolean;
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
type NormalizedImageEventType = 'loadStart' | 'load' | 'error' | 'display';
|
|
1552
|
+
|
|
1553
|
+
interface NormalizedImageEventPayload {
|
|
1554
|
+
event: NormalizedImageEventType;
|
|
1555
|
+
source: {
|
|
1556
|
+
uri: string;
|
|
1557
|
+
width: number;
|
|
1558
|
+
height: number;
|
|
1559
|
+
};
|
|
1560
|
+
cacheType: 'none' | 'disk' | 'memory';
|
|
1561
|
+
error: string;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function normalizeTextChangePayload(payload: unknown): NormalizedTextChangePayload {
|
|
1565
|
+
const normalizedSelection = normalizeTextSelectionPayload(payload);
|
|
1566
|
+
const normalizedCompositionRange = normalizeTextRangePayload(
|
|
1567
|
+
payload,
|
|
1568
|
+
'compositionRange',
|
|
1569
|
+
'compositionStart',
|
|
1570
|
+
'compositionEnd',
|
|
1571
|
+
);
|
|
1572
|
+
const isComposing = readTextComposingPayload(payload);
|
|
1573
|
+
const textRevision = readTextRevisionPayload(payload);
|
|
1574
|
+
const classification = readTextClassificationPayload(payload);
|
|
1575
|
+
if (
|
|
1576
|
+
typeof payload === 'object' &&
|
|
1577
|
+
payload !== null &&
|
|
1578
|
+
'nativeEvent' in payload &&
|
|
1579
|
+
typeof (payload as { nativeEvent?: { text?: unknown } }).nativeEvent?.text === 'string'
|
|
1580
|
+
) {
|
|
1581
|
+
return {
|
|
1582
|
+
nativeEvent: {
|
|
1583
|
+
...(payload as NormalizedTextChangePayload).nativeEvent,
|
|
1584
|
+
...(normalizedSelection ? { selection: normalizedSelection } : {}),
|
|
1585
|
+
...(isComposing !== undefined ? { isComposing } : {}),
|
|
1586
|
+
...(normalizedCompositionRange ? { compositionRange: normalizedCompositionRange } : {}),
|
|
1587
|
+
...(textRevision !== undefined ? { textRevision } : {}),
|
|
1588
|
+
...(classification !== undefined ? { classification } : {}),
|
|
1589
|
+
textChanged: readTextChangedPayload(payload),
|
|
1590
|
+
},
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
const textCandidate =
|
|
1595
|
+
typeof payload === 'object' && payload !== null
|
|
1596
|
+
? (
|
|
1597
|
+
payload as {
|
|
1598
|
+
text?: unknown;
|
|
1599
|
+
value?: unknown;
|
|
1600
|
+
}
|
|
1601
|
+
).text ??
|
|
1602
|
+
(
|
|
1603
|
+
payload as {
|
|
1604
|
+
text?: unknown;
|
|
1605
|
+
value?: unknown;
|
|
1606
|
+
}
|
|
1607
|
+
).value
|
|
1608
|
+
: payload;
|
|
1609
|
+
|
|
1610
|
+
return {
|
|
1611
|
+
nativeEvent: {
|
|
1612
|
+
text: typeof textCandidate === 'string' ? textCandidate : '',
|
|
1613
|
+
...(normalizedSelection ? { selection: normalizedSelection } : {}),
|
|
1614
|
+
...(isComposing !== undefined ? { isComposing } : {}),
|
|
1615
|
+
...(normalizedCompositionRange ? { compositionRange: normalizedCompositionRange } : {}),
|
|
1616
|
+
...(textRevision !== undefined ? { textRevision } : {}),
|
|
1617
|
+
...(classification !== undefined ? { classification } : {}),
|
|
1618
|
+
textChanged: readTextChangedPayload(payload),
|
|
1619
|
+
},
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
function readTextComposingPayload(payload: unknown): boolean | undefined {
|
|
1624
|
+
const record = payloadRecord(payload);
|
|
1625
|
+
const nativeEvent = payloadRecord(record.nativeEvent);
|
|
1626
|
+
const value = nativeEvent.isComposing ?? nativeEvent.composing ?? record.isComposing ?? record.composing;
|
|
1627
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function readTextChangedPayload(payload: unknown): boolean {
|
|
1631
|
+
const record = payloadRecord(payload);
|
|
1632
|
+
const nativeEvent = payloadRecord(record.nativeEvent);
|
|
1633
|
+
const value = nativeEvent.textChanged ?? record.textChanged;
|
|
1634
|
+
return typeof value === 'boolean' ? value : true;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function readTextRevisionPayload(payload: unknown): number | undefined {
|
|
1638
|
+
const record = payloadRecord(payload);
|
|
1639
|
+
const nativeEvent = payloadRecord(record.nativeEvent);
|
|
1640
|
+
const value = nativeEvent.textRevision ?? nativeEvent.revision ?? record.textRevision ?? record.revision;
|
|
1641
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
1642
|
+
return value;
|
|
1643
|
+
}
|
|
1644
|
+
if (typeof value === 'string') {
|
|
1645
|
+
const parsed = Number(value);
|
|
1646
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1647
|
+
}
|
|
1648
|
+
return undefined;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function readTextClassificationPayload(payload: unknown): string | undefined {
|
|
1652
|
+
const record = payloadRecord(payload);
|
|
1653
|
+
const nativeEvent = payloadRecord(record.nativeEvent);
|
|
1654
|
+
const value =
|
|
1655
|
+
nativeEvent.classification ??
|
|
1656
|
+
nativeEvent.textClassification ??
|
|
1657
|
+
record.classification ??
|
|
1658
|
+
record.textClassification;
|
|
1659
|
+
return typeof value === 'string' ? value : undefined;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function normalizeTextSelectionPayload(payload: unknown): { start: number; end: number } | undefined {
|
|
1663
|
+
return normalizeTextRangePayload(payload, 'selection', 'selectionStart', 'selectionEnd');
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function normalizeTextRangePayload(
|
|
1667
|
+
payload: unknown,
|
|
1668
|
+
rangeKey: string,
|
|
1669
|
+
startKey: string,
|
|
1670
|
+
endKey: string,
|
|
1671
|
+
): { start: number; end: number } | undefined {
|
|
1672
|
+
const record = payloadRecord(payload);
|
|
1673
|
+
const nativeEvent = payloadRecord(record.nativeEvent);
|
|
1674
|
+
const range = payloadRecord(nativeEvent[rangeKey] ?? record[rangeKey]);
|
|
1675
|
+
const startCandidate = range.start ?? nativeEvent[startKey] ?? record[startKey];
|
|
1676
|
+
const endCandidate = range.end ?? nativeEvent[endKey] ?? record[endKey];
|
|
1677
|
+
|
|
1678
|
+
if (
|
|
1679
|
+
typeof startCandidate !== 'number' ||
|
|
1680
|
+
typeof endCandidate !== 'number' ||
|
|
1681
|
+
!Number.isFinite(startCandidate) ||
|
|
1682
|
+
!Number.isFinite(endCandidate)
|
|
1683
|
+
) {
|
|
1684
|
+
return undefined;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
return {
|
|
1688
|
+
start: startCandidate,
|
|
1689
|
+
end: endCandidate,
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function payloadRecord(payload: unknown): Record<string, unknown> {
|
|
1694
|
+
return typeof payload === 'object' && payload !== null
|
|
1695
|
+
? payload as Record<string, unknown>
|
|
1696
|
+
: {};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function readEventField(payload: unknown, field: string): unknown {
|
|
1700
|
+
const record = payloadRecord(payload);
|
|
1701
|
+
const nativeEvent = payloadRecord(record.nativeEvent);
|
|
1702
|
+
return nativeEvent[field] ?? record[field];
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function normalizeImageEventName(value: unknown): NormalizedImageEventType | null {
|
|
1706
|
+
if (typeof value !== 'string') {
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
switch (value.toLowerCase()) {
|
|
1711
|
+
case 'loadstart':
|
|
1712
|
+
case 'load-start':
|
|
1713
|
+
case 'load_start':
|
|
1714
|
+
return 'loadStart';
|
|
1715
|
+
case 'load':
|
|
1716
|
+
return 'load';
|
|
1717
|
+
case 'error':
|
|
1718
|
+
return 'error';
|
|
1719
|
+
case 'display':
|
|
1720
|
+
return 'display';
|
|
1721
|
+
default:
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function finiteNumber(value: unknown): number {
|
|
1727
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function normalizeImageCacheType(value: unknown): 'none' | 'disk' | 'memory' {
|
|
1731
|
+
return value === 'disk' || value === 'memory' || value === 'none' ? value : 'none';
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function normalizeImageEventPayload(payload: unknown): NormalizedImageEventPayload | null {
|
|
1735
|
+
const event = normalizeImageEventName(
|
|
1736
|
+
readEventField(payload, 'event') ?? readEventField(payload, 'type'),
|
|
1737
|
+
);
|
|
1738
|
+
if (!event) {
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const uriCandidate =
|
|
1743
|
+
readEventField(payload, 'uri') ??
|
|
1744
|
+
readEventField(payload, 'src') ??
|
|
1745
|
+
readEventField(payload, 'currentSrc');
|
|
1746
|
+
const errorCandidate =
|
|
1747
|
+
readEventField(payload, 'error') ??
|
|
1748
|
+
readEventField(payload, 'message') ??
|
|
1749
|
+
'Image failed to load.';
|
|
1750
|
+
|
|
1751
|
+
return {
|
|
1752
|
+
event,
|
|
1753
|
+
source: {
|
|
1754
|
+
uri: typeof uriCandidate === 'string' ? uriCandidate : '',
|
|
1755
|
+
width: finiteNumber(
|
|
1756
|
+
readEventField(payload, 'width') ?? readEventField(payload, 'naturalWidth'),
|
|
1757
|
+
),
|
|
1758
|
+
height: finiteNumber(
|
|
1759
|
+
readEventField(payload, 'height') ?? readEventField(payload, 'naturalHeight'),
|
|
1760
|
+
),
|
|
1761
|
+
},
|
|
1762
|
+
cacheType: normalizeImageCacheType(readEventField(payload, 'cacheType')),
|
|
1763
|
+
error: typeof errorCandidate === 'string' ? errorCandidate : 'Image failed to load.',
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
function dispatchImageEventPayload(props: Props, payload: unknown): void {
|
|
1768
|
+
const event = normalizeImageEventPayload(payload);
|
|
1769
|
+
if (!event) {
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
switch (event.event) {
|
|
1774
|
+
case 'loadStart':
|
|
1775
|
+
if (typeof props.onLoadStart === 'function') {
|
|
1776
|
+
props.onLoadStart();
|
|
1777
|
+
}
|
|
1778
|
+
return;
|
|
1779
|
+
case 'load':
|
|
1780
|
+
if (typeof props.onLoad === 'function') {
|
|
1781
|
+
props.onLoad({
|
|
1782
|
+
source: event.source,
|
|
1783
|
+
cacheType: event.cacheType,
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
return;
|
|
1787
|
+
case 'error':
|
|
1788
|
+
if (typeof props.onError === 'function') {
|
|
1789
|
+
props.onError({
|
|
1790
|
+
error: event.error,
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
return;
|
|
1794
|
+
case 'display':
|
|
1795
|
+
if (typeof props.onDisplay === 'function') {
|
|
1796
|
+
props.onDisplay();
|
|
1797
|
+
}
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function normalizeFocusChangePayload(payload: unknown): {
|
|
1803
|
+
nativeEvent: {
|
|
1804
|
+
timestamp: number;
|
|
1805
|
+
relatedTarget?: number | null;
|
|
1806
|
+
};
|
|
1807
|
+
} {
|
|
1808
|
+
const relatedTarget =
|
|
1809
|
+
(payload as { nativeEvent?: { relatedTarget?: unknown }; relatedTarget?: unknown } | null)
|
|
1810
|
+
?.nativeEvent?.relatedTarget ??
|
|
1811
|
+
(payload as { nativeEvent?: { relatedTarget?: unknown }; relatedTarget?: unknown } | null)
|
|
1812
|
+
?.relatedTarget;
|
|
1813
|
+
|
|
1814
|
+
return {
|
|
1815
|
+
nativeEvent: {
|
|
1816
|
+
timestamp: Date.now(),
|
|
1817
|
+
relatedTarget: typeof relatedTarget === 'number' ? relatedTarget : null,
|
|
1818
|
+
},
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function normalizeKeyPayload(payload: unknown): {
|
|
1823
|
+
nativeEvent: {
|
|
1824
|
+
key: string;
|
|
1825
|
+
code?: string;
|
|
1826
|
+
altKey: boolean;
|
|
1827
|
+
ctrlKey: boolean;
|
|
1828
|
+
metaKey: boolean;
|
|
1829
|
+
shiftKey: boolean;
|
|
1830
|
+
repeat: boolean;
|
|
1831
|
+
timestamp: number;
|
|
1832
|
+
};
|
|
1833
|
+
preventDefault?: () => void;
|
|
1834
|
+
defaultPrevented?: boolean;
|
|
1835
|
+
} {
|
|
1836
|
+
const nativeEvent =
|
|
1837
|
+
(payload as {
|
|
1838
|
+
nativeEvent?: {
|
|
1839
|
+
key?: unknown;
|
|
1840
|
+
code?: unknown;
|
|
1841
|
+
altKey?: unknown;
|
|
1842
|
+
ctrlKey?: unknown;
|
|
1843
|
+
metaKey?: unknown;
|
|
1844
|
+
shiftKey?: unknown;
|
|
1845
|
+
repeat?: unknown;
|
|
1846
|
+
timestamp?: unknown;
|
|
1847
|
+
};
|
|
1848
|
+
} | null)?.nativeEvent;
|
|
1849
|
+
|
|
1850
|
+
return {
|
|
1851
|
+
nativeEvent: {
|
|
1852
|
+
key: typeof nativeEvent?.key === 'string' ? nativeEvent.key : '',
|
|
1853
|
+
code: typeof nativeEvent?.code === 'string' ? nativeEvent.code : undefined,
|
|
1854
|
+
altKey: nativeEvent?.altKey === true,
|
|
1855
|
+
ctrlKey: nativeEvent?.ctrlKey === true,
|
|
1856
|
+
metaKey: nativeEvent?.metaKey === true,
|
|
1857
|
+
shiftKey: nativeEvent?.shiftKey === true,
|
|
1858
|
+
repeat: nativeEvent?.repeat === true,
|
|
1859
|
+
timestamp:
|
|
1860
|
+
typeof nativeEvent?.timestamp === 'number' && isFinite(nativeEvent.timestamp)
|
|
1861
|
+
? nativeEvent.timestamp
|
|
1862
|
+
: Date.now(),
|
|
1863
|
+
},
|
|
1864
|
+
preventDefault:
|
|
1865
|
+
typeof (payload as { preventDefault?: unknown } | null)?.preventDefault === 'function'
|
|
1866
|
+
? (payload as { preventDefault: () => void }).preventDefault
|
|
1867
|
+
: undefined,
|
|
1868
|
+
defaultPrevented:
|
|
1869
|
+
(payload as { defaultPrevented?: unknown } | null)?.defaultPrevented === true,
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function normalizeAccessibilityActionPayload(payload: unknown): {
|
|
1874
|
+
nativeEvent: {
|
|
1875
|
+
actionName: string;
|
|
1876
|
+
};
|
|
1877
|
+
} {
|
|
1878
|
+
const actionNameCandidate =
|
|
1879
|
+
(payload as { nativeEvent?: { actionName?: unknown }; actionName?: unknown } | null)
|
|
1880
|
+
?.nativeEvent?.actionName ??
|
|
1881
|
+
(payload as { nativeEvent?: { actionName?: unknown }; actionName?: unknown } | null)
|
|
1882
|
+
?.actionName;
|
|
1883
|
+
|
|
1884
|
+
return {
|
|
1885
|
+
nativeEvent: {
|
|
1886
|
+
actionName:
|
|
1887
|
+
typeof actionNameCandidate === 'string' && actionNameCandidate.length > 0
|
|
1888
|
+
? actionNameCandidate
|
|
1889
|
+
: '',
|
|
1890
|
+
},
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function readFiniteNumber(value: unknown, fallback = 0): number {
|
|
1895
|
+
if (typeof value === 'number' && isFinite(value)) {
|
|
1896
|
+
return value;
|
|
1897
|
+
}
|
|
1898
|
+
if (typeof value === 'string') {
|
|
1899
|
+
const parsed = Number(value);
|
|
1900
|
+
return isFinite(parsed) ? parsed : fallback;
|
|
1901
|
+
}
|
|
1902
|
+
return fallback;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function normalizeScrollPayload(payload: unknown): {
|
|
1906
|
+
nativeEvent: {
|
|
1907
|
+
contentOffset: {
|
|
1908
|
+
x: number;
|
|
1909
|
+
y: number;
|
|
1910
|
+
};
|
|
1911
|
+
contentSize: {
|
|
1912
|
+
width: number;
|
|
1913
|
+
height: number;
|
|
1914
|
+
};
|
|
1915
|
+
layoutMeasurement: {
|
|
1916
|
+
width: number;
|
|
1917
|
+
height: number;
|
|
1918
|
+
};
|
|
1919
|
+
timestamp: number;
|
|
1920
|
+
};
|
|
1921
|
+
} {
|
|
1922
|
+
const nativeEvent =
|
|
1923
|
+
(payload as {
|
|
1924
|
+
nativeEvent?: {
|
|
1925
|
+
contentOffset?: {
|
|
1926
|
+
x?: unknown;
|
|
1927
|
+
y?: unknown;
|
|
1928
|
+
};
|
|
1929
|
+
timestamp?: unknown;
|
|
1930
|
+
};
|
|
1931
|
+
} | null)?.nativeEvent;
|
|
1932
|
+
const contentOffset =
|
|
1933
|
+
nativeEvent?.contentOffset ??
|
|
1934
|
+
(payload as {
|
|
1935
|
+
contentOffset?: {
|
|
1936
|
+
x?: unknown;
|
|
1937
|
+
y?: unknown;
|
|
1938
|
+
};
|
|
1939
|
+
} | null)?.contentOffset;
|
|
1940
|
+
|
|
1941
|
+
return {
|
|
1942
|
+
nativeEvent: {
|
|
1943
|
+
contentOffset: {
|
|
1944
|
+
x: readFiniteNumber(contentOffset?.x ?? (payload as { x?: unknown } | null)?.x),
|
|
1945
|
+
y: readFiniteNumber(contentOffset?.y ?? (payload as { y?: unknown } | null)?.y),
|
|
1946
|
+
},
|
|
1947
|
+
contentSize: {
|
|
1948
|
+
width: readFiniteNumber(
|
|
1949
|
+
(payload as { contentSize?: { width?: unknown } } | null)?.contentSize?.width,
|
|
1950
|
+
),
|
|
1951
|
+
height: readFiniteNumber(
|
|
1952
|
+
(payload as { contentSize?: { height?: unknown } } | null)?.contentSize?.height,
|
|
1953
|
+
),
|
|
1954
|
+
},
|
|
1955
|
+
layoutMeasurement: {
|
|
1956
|
+
width: readFiniteNumber(
|
|
1957
|
+
(payload as { layoutMeasurement?: { width?: unknown } } | null)?.layoutMeasurement?.width,
|
|
1958
|
+
),
|
|
1959
|
+
height: readFiniteNumber(
|
|
1960
|
+
(payload as { layoutMeasurement?: { height?: unknown } } | null)?.layoutMeasurement?.height,
|
|
1961
|
+
),
|
|
1962
|
+
},
|
|
1963
|
+
timestamp: readFiniteNumber(nativeEvent?.timestamp, Date.now()),
|
|
1964
|
+
},
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function resolveScrollEventThrottle(props: Props): number {
|
|
1969
|
+
const value = props.scrollEventThrottle;
|
|
1970
|
+
const throttle = typeof value === 'number' && isFinite(value) ? value : 0;
|
|
1971
|
+
return throttle > 0 ? throttle : 0;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/**
|
|
1975
|
+
* Per-instance scroll throttle state. Kept outside the onScroll wrapper so an
|
|
1976
|
+
* unrelated event-prop change (which rebuilds the wrapper) does not reset the
|
|
1977
|
+
* throttle window mid-scroll.
|
|
1978
|
+
*/
|
|
1979
|
+
const scrollThrottleState = new WeakMap<ElementNode, { lastDispatchAt: number | null }>();
|
|
1980
|
+
|
|
1981
|
+
function getScrollThrottleState(instance: ElementNode): { lastDispatchAt: number | null } {
|
|
1982
|
+
let state = scrollThrottleState.get(instance);
|
|
1983
|
+
if (!state) {
|
|
1984
|
+
state = { lastDispatchAt: null };
|
|
1985
|
+
scrollThrottleState.set(instance, state);
|
|
1986
|
+
}
|
|
1987
|
+
return state;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function getElementTestId(instance: ElementNode): string | undefined {
|
|
1991
|
+
const candidate =
|
|
1992
|
+
instance.originalProps.testId ??
|
|
1993
|
+
instance.originalProps.testID ??
|
|
1994
|
+
instance.originalProps['data-testid'] ??
|
|
1995
|
+
instance.originalProps['data-test-id'] ??
|
|
1996
|
+
instance.originalProps.id;
|
|
1997
|
+
return typeof candidate === 'string' && candidate.length > 0
|
|
1998
|
+
? candidate
|
|
1999
|
+
: undefined;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function getElementLabel(instance: ElementNode): string | undefined {
|
|
2003
|
+
if (typeof instance.props.accessibilityLabel === 'string' && instance.props.accessibilityLabel.length > 0) {
|
|
2004
|
+
return instance.props.accessibilityLabel;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
if (instance.tagType === 'input') {
|
|
2008
|
+
return instance.props.value ?? instance.props.placeholder;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
return instance.props.textContent;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function syncInputFocusTracking(instance: ElementNode, focused: boolean): void {
|
|
2015
|
+
const rootId = getRootIdForNode(instance);
|
|
2016
|
+
|
|
2017
|
+
if (!focused) {
|
|
2018
|
+
clearFocusedInput(rootId, instance.id);
|
|
2019
|
+
clearFocusedTarget();
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
noteFocusedInput(rootId, instance.id);
|
|
2024
|
+
setFocusedTarget({
|
|
2025
|
+
viewId: instance.id,
|
|
2026
|
+
type: instance.originalTag,
|
|
2027
|
+
label: getElementLabel(instance),
|
|
2028
|
+
testId: getElementTestId(instance),
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
function upsertEventHandler(
|
|
2033
|
+
instance: ElementNode,
|
|
2034
|
+
eventType: EventType,
|
|
2035
|
+
handler: Function,
|
|
2036
|
+
): void {
|
|
2037
|
+
const existing = instance.events.get(eventType);
|
|
2038
|
+
if (existing) {
|
|
2039
|
+
updateHandler(existing.handlerId, handler);
|
|
2040
|
+
existing.handler = handler;
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const handlerId = registerHandler(handler);
|
|
2045
|
+
instance.events.set(eventType, { handlerId, handler });
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
function removeEventHandler(
|
|
2049
|
+
instance: ElementNode,
|
|
2050
|
+
eventType: EventType,
|
|
2051
|
+
): void {
|
|
2052
|
+
const existing = instance.events.get(eventType);
|
|
2053
|
+
if (!existing) {
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
unregisterHandler(existing.handlerId, instance.id, eventType, getRootIdForNode(instance));
|
|
2057
|
+
instance.events.delete(eventType);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function bindFunctionPropEvent(
|
|
2061
|
+
instance: ElementNode,
|
|
2062
|
+
props: Props,
|
|
2063
|
+
activeEvents: Set<EventType>,
|
|
2064
|
+
binding: (typeof PRESS_EVENT_BINDINGS)[number],
|
|
2065
|
+
): void {
|
|
2066
|
+
const handler = props[binding.propName];
|
|
2067
|
+
if (typeof handler === 'function') {
|
|
2068
|
+
activeEvents.add(binding.eventType);
|
|
2069
|
+
upsertEventHandler(instance, binding.eventType, handler);
|
|
2070
|
+
} else {
|
|
2071
|
+
removeEventHandler(instance, binding.eventType);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
function eventPropsChanged(previousProps: Props, nextProps: Props): boolean {
|
|
2076
|
+
for (const propName of EVENT_PROP_NAMES) {
|
|
2077
|
+
if (previousProps[propName] !== nextProps[propName]) {
|
|
2078
|
+
return true;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
return false;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* Shallow strict-equality check over original (framework-level) props.
|
|
2086
|
+
*
|
|
2087
|
+
* Used as the updateInstanceProps fast path: when every prop value (including
|
|
2088
|
+
* the `style` and `children` references) is identical, re-running
|
|
2089
|
+
* resolveElementState cannot produce a different result, so the whole
|
|
2090
|
+
* resolution + diff + encode pipeline can be skipped.
|
|
2091
|
+
*/
|
|
2092
|
+
function originalPropsShallowEqual(previousProps: Props, nextProps: Props): boolean {
|
|
2093
|
+
const previousKeys = Object.keys(previousProps);
|
|
2094
|
+
if (previousKeys.length !== Object.keys(nextProps).length) {
|
|
2095
|
+
return false;
|
|
2096
|
+
}
|
|
2097
|
+
for (const key of previousKeys) {
|
|
2098
|
+
if (previousProps[key] !== nextProps[key]) {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
return true;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
/**
|
|
2106
|
+
* Process event props for an element and update the handler registry.
|
|
2107
|
+
* This is framework-agnostic - each framework calls this with its props.
|
|
2108
|
+
*
|
|
2109
|
+
* IMPORTANT: This function updates handlers in place to maintain stable handler IDs.
|
|
2110
|
+
* The native side caches handler IDs from BindEvent opcodes, so we must not change
|
|
2111
|
+
* IDs unnecessarily.
|
|
2112
|
+
*/
|
|
2113
|
+
export function processEventProps(
|
|
2114
|
+
instance: ElementNode,
|
|
2115
|
+
props: Props,
|
|
2116
|
+
config: TagConfig
|
|
2117
|
+
): void {
|
|
2118
|
+
const supportsPressEvents =
|
|
2119
|
+
config.supportsPressEvents || instance.pressOverride === true;
|
|
2120
|
+
const supportsChangeEvents = config.supportsChangeEvents === true;
|
|
2121
|
+
const supportsTransitionEvents = config.supportsTransitionEvents !== false;
|
|
2122
|
+
const supportsScrollEvents = config.canonicalType === 'scroll';
|
|
2123
|
+
|
|
2124
|
+
// If this element doesn't support any events, clear all handlers
|
|
2125
|
+
if (!supportsPressEvents && !supportsChangeEvents && !supportsTransitionEvents && !supportsScrollEvents) {
|
|
2126
|
+
for (const [eventType, binding] of instance.events) {
|
|
2127
|
+
unregisterHandler(binding.handlerId, instance.id, eventType, getRootIdForNode(instance));
|
|
2128
|
+
}
|
|
2129
|
+
instance.events.clear();
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// Track which events are present in props
|
|
2134
|
+
const activeEvents = new Set<EventType>();
|
|
2135
|
+
|
|
2136
|
+
if (supportsPressEvents) {
|
|
2137
|
+
for (const binding of PRESS_EVENT_BINDINGS) {
|
|
2138
|
+
bindFunctionPropEvent(instance, props, activeEvents, binding);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// Process change events (for toggles, inputs)
|
|
2143
|
+
if (supportsChangeEvents) {
|
|
2144
|
+
if (instance.tagType === 'image') {
|
|
2145
|
+
const hasImageEventHandler =
|
|
2146
|
+
typeof props.onLoadStart === 'function' ||
|
|
2147
|
+
typeof props.onLoad === 'function' ||
|
|
2148
|
+
typeof props.onError === 'function' ||
|
|
2149
|
+
typeof props.onDisplay === 'function';
|
|
2150
|
+
|
|
2151
|
+
if (hasImageEventHandler) {
|
|
2152
|
+
activeEvents.add(EventType.Change);
|
|
2153
|
+
upsertEventHandler(instance, EventType.Change, (payload: unknown) => {
|
|
2154
|
+
dispatchImageEventPayload(props, payload);
|
|
2155
|
+
});
|
|
2156
|
+
} else {
|
|
2157
|
+
removeEventHandler(instance, EventType.Change);
|
|
2158
|
+
}
|
|
2159
|
+
} else if (instance.tagType === 'nativeView') {
|
|
2160
|
+
const moduleName = instance.props.nativeViewModuleName;
|
|
2161
|
+
const isToggleNativeView = moduleName === 'exact.toggle';
|
|
2162
|
+
const hasNativeViewChangeHandler =
|
|
2163
|
+
typeof props.onChange === 'function' ||
|
|
2164
|
+
(isToggleNativeView && typeof props.onValueChange === 'function');
|
|
2165
|
+
|
|
2166
|
+
if (hasNativeViewChangeHandler) {
|
|
2167
|
+
activeEvents.add(EventType.Change);
|
|
2168
|
+
|
|
2169
|
+
// Native-view controls report already-structured payloads from the host
|
|
2170
|
+
// (for example `{ value: 0.42 }` for sliders). Unlike text inputs, this
|
|
2171
|
+
// payload should not be coerced into a text event shape.
|
|
2172
|
+
const changeHandler = (payload: unknown) => {
|
|
2173
|
+
if (isToggleNativeView && typeof props.onValueChange === 'function') {
|
|
2174
|
+
const value =
|
|
2175
|
+
(payload as { nativeEvent?: { value?: unknown }; value?: unknown } | null)?.nativeEvent?.value ??
|
|
2176
|
+
(payload as { nativeEvent?: { value?: unknown }; value?: unknown } | null)?.value ??
|
|
2177
|
+
payload;
|
|
2178
|
+
props.onValueChange(value as boolean);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
const onChange = props.onChange as ((event: unknown) => void) | undefined;
|
|
2182
|
+
onChange?.(payload);
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
upsertEventHandler(instance, EventType.Change, changeHandler);
|
|
2186
|
+
|
|
2187
|
+
if (typeof instance.props.nativeViewModuleName === 'string') {
|
|
2188
|
+
registerNativeViewEventHandler(
|
|
2189
|
+
instance.id,
|
|
2190
|
+
instance.props.nativeViewModuleName,
|
|
2191
|
+
(eventName, payload) => {
|
|
2192
|
+
if (eventName !== 'change') {
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
return dispatchSyntheticEvent(instance, EventType.Change, payload);
|
|
2196
|
+
},
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
} else {
|
|
2200
|
+
unregisterNativeViewEventHandler(instance.id);
|
|
2201
|
+
removeEventHandler(instance, EventType.Change);
|
|
2202
|
+
}
|
|
2203
|
+
} else {
|
|
2204
|
+
const hasToggleChangeHandler = typeof props.onValueChange === 'function';
|
|
2205
|
+
const hasTextChangeHandler =
|
|
2206
|
+
typeof props.onChangeText === 'function' ||
|
|
2207
|
+
typeof props.onChange === 'function' ||
|
|
2208
|
+
typeof props.onSelectionChange === 'function';
|
|
2209
|
+
|
|
2210
|
+
if (hasToggleChangeHandler || hasTextChangeHandler) {
|
|
2211
|
+
activeEvents.add(EventType.Change);
|
|
2212
|
+
|
|
2213
|
+
const changeHandler = (payload: unknown) => {
|
|
2214
|
+
if (typeof props.onValueChange === 'function') {
|
|
2215
|
+
const value =
|
|
2216
|
+
(payload as { nativeEvent?: { value?: unknown }; value?: unknown } | null)?.nativeEvent?.value ??
|
|
2217
|
+
(payload as { nativeEvent?: { value?: unknown }; value?: unknown } | null)?.value ??
|
|
2218
|
+
payload;
|
|
2219
|
+
props.onValueChange(value as boolean);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
if (typeof props.onChangeText === 'function' || typeof props.onChange === 'function') {
|
|
2223
|
+
const event = normalizeTextChangePayload(payload);
|
|
2224
|
+
const shouldDispatchCommittedText =
|
|
2225
|
+
event.nativeEvent.textChanged !== false &&
|
|
2226
|
+
event.nativeEvent.isComposing !== true;
|
|
2227
|
+
if (shouldDispatchCommittedText && typeof props.onChangeText === 'function') {
|
|
2228
|
+
props.onChangeText(event.nativeEvent.text);
|
|
2229
|
+
}
|
|
2230
|
+
if (shouldDispatchCommittedText && typeof props.onChange === 'function') {
|
|
2231
|
+
props.onChange(event);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (typeof props.onSelectionChange === 'function') {
|
|
2236
|
+
const event = normalizeTextChangePayload(payload);
|
|
2237
|
+
if (event.nativeEvent.selection) {
|
|
2238
|
+
props.onSelectionChange({
|
|
2239
|
+
nativeEvent: {
|
|
2240
|
+
selection: event.nativeEvent.selection,
|
|
2241
|
+
text: event.nativeEvent.text,
|
|
2242
|
+
...(event.nativeEvent.isComposing !== undefined
|
|
2243
|
+
? { isComposing: event.nativeEvent.isComposing }
|
|
2244
|
+
: {}),
|
|
2245
|
+
...(event.nativeEvent.compositionRange
|
|
2246
|
+
? { compositionRange: event.nativeEvent.compositionRange }
|
|
2247
|
+
: {}),
|
|
2248
|
+
...(event.nativeEvent.textRevision !== undefined
|
|
2249
|
+
? { textRevision: event.nativeEvent.textRevision }
|
|
2250
|
+
: {}),
|
|
2251
|
+
...(event.nativeEvent.classification !== undefined
|
|
2252
|
+
? { classification: event.nativeEvent.classification }
|
|
2253
|
+
: {}),
|
|
2254
|
+
},
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
|
|
2260
|
+
upsertEventHandler(instance, EventType.Change, changeHandler);
|
|
2261
|
+
} else {
|
|
2262
|
+
// Remove change handler if no longer in props
|
|
2263
|
+
removeEventHandler(instance, EventType.Change);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
if (supportsTransitionEvents) {
|
|
2269
|
+
if (typeof props.onTransitionEnd === 'function') {
|
|
2270
|
+
activeEvents.add(EventType.TransitionEnd);
|
|
2271
|
+
upsertEventHandler(instance, EventType.TransitionEnd, props.onTransitionEnd);
|
|
2272
|
+
} else {
|
|
2273
|
+
removeEventHandler(instance, EventType.TransitionEnd);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
if (supportsScrollEvents) {
|
|
2278
|
+
if (typeof props.onScroll === 'function') {
|
|
2279
|
+
activeEvents.add(EventType.Scroll);
|
|
2280
|
+
const onScroll = props.onScroll as (event: ReturnType<typeof normalizeScrollPayload>) => void;
|
|
2281
|
+
const scrollEventThrottle = resolveScrollEventThrottle(props);
|
|
2282
|
+
const throttleState = getScrollThrottleState(instance);
|
|
2283
|
+
upsertEventHandler(instance, EventType.Scroll, (payload: unknown) => {
|
|
2284
|
+
const event = normalizeScrollPayload(payload);
|
|
2285
|
+
const timestamp = event.nativeEvent.timestamp;
|
|
2286
|
+
if (
|
|
2287
|
+
scrollEventThrottle > 0 &&
|
|
2288
|
+
throttleState.lastDispatchAt !== null &&
|
|
2289
|
+
timestamp >= throttleState.lastDispatchAt &&
|
|
2290
|
+
timestamp - throttleState.lastDispatchAt < scrollEventThrottle
|
|
2291
|
+
) {
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
throttleState.lastDispatchAt = timestamp;
|
|
2295
|
+
onScroll(event);
|
|
2296
|
+
});
|
|
2297
|
+
} else {
|
|
2298
|
+
removeEventHandler(instance, EventType.Scroll);
|
|
2299
|
+
scrollThrottleState.delete(instance);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
for (const binding of FOCUS_AND_KEYBOARD_BINDINGS) {
|
|
2304
|
+
const handler = props[binding.propName];
|
|
2305
|
+
const submitEditingHandler =
|
|
2306
|
+
instance.tagType === 'input' && binding.eventType === EventType.KeyDown
|
|
2307
|
+
? props.onSubmitEditing
|
|
2308
|
+
: undefined;
|
|
2309
|
+
const needsInternalInputTracking =
|
|
2310
|
+
instance.tagType === 'input' &&
|
|
2311
|
+
(binding.eventType === EventType.Focus || binding.eventType === EventType.Blur);
|
|
2312
|
+
|
|
2313
|
+
if (
|
|
2314
|
+
typeof handler === 'function' ||
|
|
2315
|
+
typeof submitEditingHandler === 'function' ||
|
|
2316
|
+
needsInternalInputTracking
|
|
2317
|
+
) {
|
|
2318
|
+
activeEvents.add(binding.eventType);
|
|
2319
|
+
upsertEventHandler(instance, binding.eventType, (payload: unknown) => {
|
|
2320
|
+
const normalizedPayload = binding.normalize(payload);
|
|
2321
|
+
if (binding.eventType === EventType.Focus) {
|
|
2322
|
+
syncInputFocusTracking(instance, true);
|
|
2323
|
+
} else if (binding.eventType === EventType.Blur) {
|
|
2324
|
+
syncInputFocusTracking(instance, false);
|
|
2325
|
+
}
|
|
2326
|
+
if (typeof handler === 'function') {
|
|
2327
|
+
handler(normalizedPayload);
|
|
2328
|
+
}
|
|
2329
|
+
if (
|
|
2330
|
+
typeof submitEditingHandler === 'function' &&
|
|
2331
|
+
binding.eventType === EventType.KeyDown &&
|
|
2332
|
+
(normalizedPayload as { nativeEvent: { key?: string } }).nativeEvent.key === 'Enter' &&
|
|
2333
|
+
props.multiline !== true
|
|
2334
|
+
) {
|
|
2335
|
+
submitEditingHandler();
|
|
2336
|
+
}
|
|
2337
|
+
});
|
|
2338
|
+
} else {
|
|
2339
|
+
removeEventHandler(instance, binding.eventType);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// Remove handlers for events no longer in props
|
|
2344
|
+
for (const [eventType, binding] of instance.events) {
|
|
2345
|
+
if (!activeEvents.has(eventType)) {
|
|
2346
|
+
unregisterHandler(binding.handlerId, instance.id, eventType, getRootIdForNode(instance));
|
|
2347
|
+
instance.events.delete(eventType);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
/**
|
|
2353
|
+
* Recursively clean up event handlers for a node and all its children.
|
|
2354
|
+
* This ensures native event listeners are properly released.
|
|
2355
|
+
*/
|
|
2356
|
+
export function cleanupNodeHandlers(
|
|
2357
|
+
node: ElementNode | TextNode,
|
|
2358
|
+
rootId: number = getRootIdForNode(node),
|
|
2359
|
+
): void {
|
|
2360
|
+
if (node.kind === NodeKind.Element) {
|
|
2361
|
+
const element = node as ElementNode;
|
|
2362
|
+
|
|
2363
|
+
if (element.tagType === 'nativeView') {
|
|
2364
|
+
unregisterNativeViewEventHandler(element.id);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// Unregister all event handlers for this node
|
|
2368
|
+
for (const [eventType, binding] of element.events) {
|
|
2369
|
+
unregisterHandler(binding.handlerId, element.id, eventType, rootId);
|
|
2370
|
+
}
|
|
2371
|
+
element.events.clear();
|
|
2372
|
+
|
|
2373
|
+
// Recursively clean up children
|
|
2374
|
+
for (const child of element.children) {
|
|
2375
|
+
cleanupNodeHandlers(child, rootId);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// =============================================================================
|
|
2381
|
+
// High-Level Host Operations
|
|
2382
|
+
// =============================================================================
|
|
2383
|
+
|
|
2384
|
+
/**
|
|
2385
|
+
* Create an element instance.
|
|
2386
|
+
* Used by framework adapters during React's render phase, so this must remain side-effect free.
|
|
2387
|
+
*/
|
|
2388
|
+
export function createInstance(
|
|
2389
|
+
type: string,
|
|
2390
|
+
props: Props,
|
|
2391
|
+
): ElementNode {
|
|
2392
|
+
const config = getTagConfig(type) ?? defaultTagConfig;
|
|
2393
|
+
|
|
2394
|
+
// Create the element node
|
|
2395
|
+
const instance = createElementNode(
|
|
2396
|
+
config.canonicalType,
|
|
2397
|
+
type,
|
|
2398
|
+
config.isRNStyle,
|
|
2399
|
+
props
|
|
2400
|
+
);
|
|
2401
|
+
|
|
2402
|
+
applyResolvedElementState(
|
|
2403
|
+
instance,
|
|
2404
|
+
resolveElementState(instance, config, type, props, getRootInheritedState(0)),
|
|
2405
|
+
);
|
|
2406
|
+
|
|
2407
|
+
// Event handlers are intentionally NOT registered here: React can abort a
|
|
2408
|
+
// render and drop the instance, which would leak entries in the global
|
|
2409
|
+
// handler registry. Registration happens at commit, in encodeCreatedSubtree.
|
|
2410
|
+
return instance;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/**
|
|
2414
|
+
* Create a text instance.
|
|
2415
|
+
* Used by framework adapters during React's render phase, so this must remain side-effect free.
|
|
2416
|
+
*/
|
|
2417
|
+
export function createTextInstance(text: string): TextNode {
|
|
2418
|
+
return createTextNode(text);
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
function encodeCreatedSubtree(
|
|
2422
|
+
encoder: ProtocolEncoder,
|
|
2423
|
+
node: ElementNode | TextNode,
|
|
2424
|
+
inheritedState: InheritedElementState = getRootInheritedState(),
|
|
2425
|
+
inheritedTextMeasureStyle: CanonicalStyle | null = null,
|
|
2426
|
+
): void {
|
|
2427
|
+
if (!hasDirtyFlag(node, DirtyFlags.Created)) {
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
if (node.kind === NodeKind.Text) {
|
|
2432
|
+
encodeCreateText(encoder, node);
|
|
2433
|
+
if (inheritedTextMeasureStyle) {
|
|
2434
|
+
setStyle(encoder, node.id, inheritedTextMeasureStyle);
|
|
2435
|
+
}
|
|
2436
|
+
markViewChanged(node);
|
|
2437
|
+
clearDirtyFlags(node);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
const nodeTextMeasureStyle = inheritedTextMeasureStyleForChild(
|
|
2442
|
+
node,
|
|
2443
|
+
inheritedTextMeasureStyle,
|
|
2444
|
+
);
|
|
2445
|
+
|
|
2446
|
+
const config = getTagConfig(node.originalTag) ?? defaultTagConfig;
|
|
2447
|
+
// Commit-time event registration (createInstance is render-phase pure).
|
|
2448
|
+
// processEventProps is idempotent, so adapters that already registered
|
|
2449
|
+
// handlers imperatively (DOM shim, Solid, Vue) keep their stable handler IDs.
|
|
2450
|
+
processEventProps(node, node.originalProps, config);
|
|
2451
|
+
if (canRefreshCreatedMountEnvironment(node, inheritedState)) {
|
|
2452
|
+
refreshCreatedMountEnvironment(node, inheritedState);
|
|
2453
|
+
} else {
|
|
2454
|
+
applyResolvedElementState(
|
|
2455
|
+
node,
|
|
2456
|
+
resolveElementState(node, config, node.originalTag, node.originalProps, inheritedState),
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
encodeCreateElement(encoder, node);
|
|
2460
|
+
if (inheritedTextMeasureStyle && node.tagType === 'text' && nodeTextMeasureStyle) {
|
|
2461
|
+
setStyle(encoder, node.id, nodeTextMeasureStyle);
|
|
2462
|
+
}
|
|
2463
|
+
markViewChanged(node);
|
|
2464
|
+
const childInheritedState: InheritedElementState = {
|
|
2465
|
+
rootId: node.propagatedSafeArea?.rootId ?? inheritedState.rootId,
|
|
2466
|
+
direction: node.resolvedDirection,
|
|
2467
|
+
writingMode: node.resolvedWritingMode,
|
|
2468
|
+
lang: node.resolvedLang,
|
|
2469
|
+
safeArea: node.propagatedSafeArea ?? inheritedState.safeArea,
|
|
2470
|
+
};
|
|
2471
|
+
for (const child of node.children) {
|
|
2472
|
+
encodeCreatedSubtree(encoder, child, childInheritedState, nodeTextMeasureStyle);
|
|
2473
|
+
}
|
|
2474
|
+
if (node.children.length > 0) {
|
|
2475
|
+
encodeChildren(encoder, node);
|
|
2476
|
+
}
|
|
2477
|
+
clearDirtyFlags(node);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
function syncInheritedStateInSubtree(
|
|
2481
|
+
encoder: ProtocolEncoder,
|
|
2482
|
+
node: ElementNode | TextNode,
|
|
2483
|
+
): void {
|
|
2484
|
+
if (node.kind === NodeKind.Text) {
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
const config = getTagConfig(node.originalTag) ?? defaultTagConfig;
|
|
2489
|
+
const previousStyle = node.style;
|
|
2490
|
+
const previousProps = node.props;
|
|
2491
|
+
const previousTransitions = node.transitions;
|
|
2492
|
+
const previousDirection = node.resolvedDirection;
|
|
2493
|
+
const previousWritingMode = node.resolvedWritingMode;
|
|
2494
|
+
const previousLang = node.resolvedLang;
|
|
2495
|
+
const previousSafeArea = node.propagatedSafeArea;
|
|
2496
|
+
const nextState = resolveElementState(
|
|
2497
|
+
node,
|
|
2498
|
+
config,
|
|
2499
|
+
node.originalTag,
|
|
2500
|
+
node.originalProps,
|
|
2501
|
+
getInheritedState(node.parent as ElementNode | RootNode | null),
|
|
2502
|
+
);
|
|
2503
|
+
const changedStyleKeys = collectChangedStyleKeys(previousStyle, nextState.style);
|
|
2504
|
+
const styleChanged = changedStyleKeys.length > 0;
|
|
2505
|
+
const propsChanged = !propsEqual(previousProps, nextState.props);
|
|
2506
|
+
const transitionsChanged = !transitionsEqual(previousTransitions, nextState.transitions);
|
|
2507
|
+
const inheritedStateChanged =
|
|
2508
|
+
previousDirection !== nextState.resolvedDirection ||
|
|
2509
|
+
previousWritingMode !== nextState.resolvedWritingMode ||
|
|
2510
|
+
previousLang !== nextState.resolvedLang ||
|
|
2511
|
+
!safeAreaPropagationEqual(previousSafeArea, nextState.propagatedSafeArea);
|
|
2512
|
+
|
|
2513
|
+
applyResolvedElementState(node, nextState);
|
|
2514
|
+
|
|
2515
|
+
if (!hasDirtyFlag(node, DirtyFlags.Created)) {
|
|
2516
|
+
if (styleChanged) {
|
|
2517
|
+
markViewChanged(node);
|
|
2518
|
+
if (styleKeysAffectLayout(changedStyleKeys)) {
|
|
2519
|
+
markLayoutDirtyForNode(node);
|
|
2520
|
+
}
|
|
2521
|
+
encodeStyleUpdate(encoder, node.id, changedStyleKeys, node.style);
|
|
2522
|
+
if (node.tagType === 'text' && styleKeysAffectTextLeafMeasure(changedStyleKeys)) {
|
|
2523
|
+
encodeTextLeafMeasureStyles(encoder, node);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
if (transitionsChanged) {
|
|
2528
|
+
markViewChanged(node);
|
|
2529
|
+
setTransition(encoder, node.id, node.transitions);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
if (propsChanged) {
|
|
2533
|
+
markViewChanged(node);
|
|
2534
|
+
getPendingCommitState(getRootIdForNode(node)).warningAnalysisDirty = true;
|
|
2535
|
+
encodeProps(encoder, node);
|
|
2536
|
+
} else if (
|
|
2537
|
+
previousLang !== node.resolvedLang &&
|
|
2538
|
+
typeof node.props.lang === 'string' &&
|
|
2539
|
+
node.props.lang.length > 0
|
|
2540
|
+
) {
|
|
2541
|
+
markViewChanged(node);
|
|
2542
|
+
setLang(encoder, node.id, node.resolvedLang);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
if (!inheritedStateChanged) {
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
for (const child of node.children) {
|
|
2551
|
+
syncInheritedStateInSubtree(encoder, child);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
function syncSelectablePropsInSubtree(
|
|
2556
|
+
encoder: ProtocolEncoder,
|
|
2557
|
+
node: ElementNode | TextNode,
|
|
2558
|
+
): void {
|
|
2559
|
+
if (node.kind === NodeKind.Text) {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
if (
|
|
2564
|
+
!hasDirtyFlag(node, DirtyFlags.Created) &&
|
|
2565
|
+
(node.tagType === 'text' || node.props.selectable !== undefined)
|
|
2566
|
+
) {
|
|
2567
|
+
encodeProps(encoder, node);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
for (const child of node.children) {
|
|
2571
|
+
syncSelectablePropsInSubtree(encoder, child);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
function hasCommittedViewportSizeChanged(rootId: number): boolean {
|
|
2576
|
+
const currentViewport = getScreenDimensions(rootId);
|
|
2577
|
+
const previousViewport = committedRootViewportSizes.get(rootId);
|
|
2578
|
+
return (
|
|
2579
|
+
previousViewport === undefined ||
|
|
2580
|
+
previousViewport.width !== currentViewport.width ||
|
|
2581
|
+
previousViewport.height !== currentViewport.height
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
function syncRootInheritedState(root: RootNode): void {
|
|
2586
|
+
const enc = getEncoderForRoot(root.rootId);
|
|
2587
|
+
const opCountBefore = enc.getOpCount();
|
|
2588
|
+
|
|
2589
|
+
for (const child of root.children) {
|
|
2590
|
+
syncInheritedStateInSubtree(enc, child);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
if (enc.getOpCount() > opCountBefore) {
|
|
2594
|
+
commitBatch(root);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
function syncLiveRootInheritedState(): void {
|
|
2599
|
+
for (const root of liveRoots.values()) {
|
|
2600
|
+
syncRootInheritedState(root);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
/**
|
|
2605
|
+
* Re-resolve inherited layout state for an already-mounted root after the host
|
|
2606
|
+
* environment changes.
|
|
2607
|
+
*
|
|
2608
|
+
* Window state changes (viewport size, safe area insets, keyboard metrics)
|
|
2609
|
+
* affect style resolution even when JSX/template props stay byte-for-byte
|
|
2610
|
+
* identical. Adapters call this helper from their root window subscriptions so
|
|
2611
|
+
* host-ops can recompute the affected styles without relying on framework prop
|
|
2612
|
+
* diffs.
|
|
2613
|
+
*/
|
|
2614
|
+
export function syncRootWindowState(root: RootNode): void {
|
|
2615
|
+
const enc = getEncoderForRoot(root.rootId);
|
|
2616
|
+
|
|
2617
|
+
for (const child of root.children) {
|
|
2618
|
+
syncInheritedStateInSubtree(enc, child);
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
commitBatch(root);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
export function syncRootInheritedWindowState(root: RootNode): void {
|
|
2625
|
+
const enc = getEncoderForRoot(root.rootId);
|
|
2626
|
+
const opCountBefore = enc.getOpCount();
|
|
2627
|
+
const viewportChanged = hasCommittedViewportSizeChanged(root.rootId);
|
|
2628
|
+
|
|
2629
|
+
for (const child of root.children) {
|
|
2630
|
+
syncInheritedStateInSubtree(enc, child);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
if (enc.getOpCount() > opCountBefore) {
|
|
2634
|
+
commitBatch(root);
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
if (viewportChanged) {
|
|
2639
|
+
committedRootViewportSizes.set(root.rootId, getScreenDimensions(root.rootId));
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/**
|
|
2644
|
+
* Append a child to a parent and encode the operation.
|
|
2645
|
+
*/
|
|
2646
|
+
export function appendChild(parent: ElementNode | RootNode, child: ElementNode | TextNode): void {
|
|
2647
|
+
nodeAppendChild(parent, child as ElementNode);
|
|
2648
|
+
const enc = getEncoderForNode(parent);
|
|
2649
|
+
// Selectable re-sync must run while Created flags are still intact: its
|
|
2650
|
+
// !Created gate skips nodes that encodeCreatedSubtree is about to encode in
|
|
2651
|
+
// full, so only pre-existing (reparented) nodes get their props re-emitted.
|
|
2652
|
+
syncSelectablePropsInSubtree(enc, child);
|
|
2653
|
+
encodeCreatedSubtree(
|
|
2654
|
+
enc,
|
|
2655
|
+
child,
|
|
2656
|
+
getInheritedState(parent),
|
|
2657
|
+
inheritedTextMeasureStyleForInsertedChildren(parent),
|
|
2658
|
+
);
|
|
2659
|
+
markChildrenDirty(parent);
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
/**
|
|
2663
|
+
* Insert a child before another child and encode the operation.
|
|
2664
|
+
*/
|
|
2665
|
+
export function insertBefore(
|
|
2666
|
+
parent: ElementNode | RootNode,
|
|
2667
|
+
child: ElementNode | TextNode,
|
|
2668
|
+
beforeChild: ElementNode | TextNode
|
|
2669
|
+
): void {
|
|
2670
|
+
nodeInsertBefore(parent, child as ElementNode, beforeChild as ElementNode);
|
|
2671
|
+
const enc = getEncoderForNode(parent);
|
|
2672
|
+
// See appendChild: run before encoding so the !Created gate is meaningful.
|
|
2673
|
+
syncSelectablePropsInSubtree(enc, child);
|
|
2674
|
+
encodeCreatedSubtree(
|
|
2675
|
+
enc,
|
|
2676
|
+
child,
|
|
2677
|
+
getInheritedState(parent),
|
|
2678
|
+
inheritedTextMeasureStyleForInsertedChildren(parent),
|
|
2679
|
+
);
|
|
2680
|
+
markChildrenDirty(parent);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
/**
|
|
2684
|
+
* Detach a child from its parent without destroying the subtree.
|
|
2685
|
+
*
|
|
2686
|
+
* This is the DOM-spec-style "remove from this parent, but keep the node alive"
|
|
2687
|
+
* primitive required for RFC 0043 reparenting. Event handlers stay registered
|
|
2688
|
+
* and descendants keep their identity so the subtree can be inserted again.
|
|
2689
|
+
*/
|
|
2690
|
+
export function detachChild(parent: ElementNode | RootNode, child: ElementNode | TextNode): void {
|
|
2691
|
+
nodeRemoveChild(parent, child as ElementNode);
|
|
2692
|
+
markChildrenDirty(parent);
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
/**
|
|
2696
|
+
* Remove a child from a parent and destroy the detached subtree.
|
|
2697
|
+
*
|
|
2698
|
+
* The destructive behavior here is deliberate: existing React/Solid/Vue host
|
|
2699
|
+
* configs use removeChild for final removal. RFC 0043 adds detachChild for the
|
|
2700
|
+
* non-destructive DOM-shim path.
|
|
2701
|
+
*/
|
|
2702
|
+
export function removeChild(parent: ElementNode | RootNode, child: ElementNode | TextNode): void {
|
|
2703
|
+
const enc = getEncoderForNode(parent);
|
|
2704
|
+
const rootId = getRootIdForNode(parent);
|
|
2705
|
+
// Clean up event handlers
|
|
2706
|
+
cleanupNodeHandlers(child, rootId);
|
|
2707
|
+
nodeRemoveChild(parent, child as ElementNode);
|
|
2708
|
+
if (!hasDirtyFlag(child, DirtyFlags.Created)) {
|
|
2709
|
+
// Pass rootId explicitly: the child is already severed from the tree, so
|
|
2710
|
+
// walking up from it would misattribute the destroyed views to root 0.
|
|
2711
|
+
destroySubtree(child, enc, rootId);
|
|
2712
|
+
}
|
|
2713
|
+
markChildrenDirty(parent);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
/**
|
|
2717
|
+
* Clear all children from a container and encode the operation.
|
|
2718
|
+
*/
|
|
2719
|
+
export function clearContainer(container: RootNode): void {
|
|
2720
|
+
const enc = getEncoderForNode(container);
|
|
2721
|
+
const rootId = container.rootId;
|
|
2722
|
+
// Clean up all event handlers and destroy children
|
|
2723
|
+
for (const child of container.children.slice()) {
|
|
2724
|
+
cleanupNodeHandlers(child, rootId);
|
|
2725
|
+
if (!hasDirtyFlag(child, DirtyFlags.Created)) {
|
|
2726
|
+
destroySubtree(child, enc, rootId);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
nodeClearChildren(container);
|
|
2731
|
+
markChildrenDirty(container);
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
/**
|
|
2735
|
+
* Destroy a root and flush the teardown batch before releasing its encoder.
|
|
2736
|
+
*
|
|
2737
|
+
* This is intentionally per-root instead of global reset logic so DOM handles,
|
|
2738
|
+
* multi-root adapters, and HMR teardown can remove one root without affecting
|
|
2739
|
+
* any others.
|
|
2740
|
+
*/
|
|
2741
|
+
export function destroyRoot(rootId: number): void {
|
|
2742
|
+
const root = liveRoots.get(rootId);
|
|
2743
|
+
const enc = encoders.get(rootId);
|
|
2744
|
+
|
|
2745
|
+
if (!root) {
|
|
2746
|
+
if (enc) {
|
|
2747
|
+
dispatchProtocol(enc);
|
|
2748
|
+
encoders.delete(rootId);
|
|
2749
|
+
}
|
|
2750
|
+
unregisterInspectorRoot(rootId);
|
|
2751
|
+
rootOwners.delete(rootId);
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
clearContainer(root);
|
|
2756
|
+
const encoder = getEncoderForRoot(rootId);
|
|
2757
|
+
flushDirtyChildren(encoder, pendingCommitStateByRoot.get(rootId));
|
|
2758
|
+
dispatchProtocol(encoder);
|
|
2759
|
+
encoders.delete(rootId);
|
|
2760
|
+
pendingCommitStateByRoot.delete(rootId);
|
|
2761
|
+
unregisterInspectorRoot(rootId);
|
|
2762
|
+
rootOwners.delete(rootId);
|
|
2763
|
+
(globalThis as typeof globalThis & {
|
|
2764
|
+
__exactDomMirror?: { remove(rootId: number): void };
|
|
2765
|
+
}).__exactDomMirror?.remove(rootId);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
/**
|
|
2769
|
+
* Recursively destroy a node and all of its descendants.
|
|
2770
|
+
*
|
|
2771
|
+
* Important: we must encode destroys bottom-up so parents don't temporarily
|
|
2772
|
+
* reference children that no longer exist during intermediate batches.
|
|
2773
|
+
*/
|
|
2774
|
+
function destroySubtree(
|
|
2775
|
+
node: ElementNode | TextNode,
|
|
2776
|
+
enc?: ProtocolEncoder,
|
|
2777
|
+
rootId: number = getRootIdForNode(node),
|
|
2778
|
+
): void {
|
|
2779
|
+
const encoder = enc ?? getEncoderForRoot(rootId);
|
|
2780
|
+
if (node.kind === NodeKind.Element) {
|
|
2781
|
+
// Copy because children array is still used by node ops.
|
|
2782
|
+
const children = (node as ElementNode).children.slice();
|
|
2783
|
+
for (const child of children) {
|
|
2784
|
+
destroySubtree(child, encoder, rootId);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
// The owning rootId is threaded through (like cleanupNodeHandlers) because
|
|
2789
|
+
// the subtree may already be detached, which would resolve to root 0.
|
|
2790
|
+
getPendingCommitState(rootId).changedViews.add(node.id);
|
|
2791
|
+
encodeDestroy(encoder, node);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
/**
|
|
2795
|
+
* Update an element's props and encode changes.
|
|
2796
|
+
*/
|
|
2797
|
+
export function updateInstanceProps(
|
|
2798
|
+
instance: ElementNode,
|
|
2799
|
+
oldProps: Props,
|
|
2800
|
+
newProps: Props
|
|
2801
|
+
): void {
|
|
2802
|
+
const config = getTagConfig(instance.originalTag) ?? defaultTagConfig;
|
|
2803
|
+
|
|
2804
|
+
// Update style if changed
|
|
2805
|
+
// NOTE: React 19 may pass the same props object for both oldProps and newProps,
|
|
2806
|
+
// so we compare against the stored instance.style instead of oldProps.style
|
|
2807
|
+
const previousProps = instance.originalProps ?? oldProps;
|
|
2808
|
+
|
|
2809
|
+
// Fast path: React 19 commits a host update whenever the props object
|
|
2810
|
+
// identity changes, even if every prop value is unchanged (a parent
|
|
2811
|
+
// re-render recreates the element). Shallow-equal props (style and children
|
|
2812
|
+
// compared by identity) cannot change the resolved state, so skip the full
|
|
2813
|
+
// resolveElementState pipeline. The same-reference case must NOT take this
|
|
2814
|
+
// path: the DOM shim mutates its live props object in place and relies on
|
|
2815
|
+
// full re-resolution to discover changes.
|
|
2816
|
+
if (previousProps !== newProps && originalPropsShallowEqual(previousProps, newProps)) {
|
|
2817
|
+
instance.originalProps = newProps;
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
const oldCanonicalProps = instance.props;
|
|
2822
|
+
const oldStyle = instance.style;
|
|
2823
|
+
const oldResolvedDirection = instance.resolvedDirection;
|
|
2824
|
+
const oldResolvedWritingMode = instance.resolvedWritingMode;
|
|
2825
|
+
const oldResolvedLang = instance.resolvedLang;
|
|
2826
|
+
const oldPropagatedSafeArea = instance.propagatedSafeArea;
|
|
2827
|
+
const oldTransitions = instance.transitions;
|
|
2828
|
+
const oldParagraphKey = instance.paragraphKey;
|
|
2829
|
+
const nextState = resolveElementState(
|
|
2830
|
+
instance,
|
|
2831
|
+
config,
|
|
2832
|
+
instance.originalTag,
|
|
2833
|
+
newProps,
|
|
2834
|
+
getInheritedState(instance.parent as ElementNode | RootNode | null),
|
|
2835
|
+
);
|
|
2836
|
+
const newCanonicalProps = nextState.props;
|
|
2837
|
+
const newStyle = nextState.style;
|
|
2838
|
+
const newTransitions = nextState.transitions;
|
|
2839
|
+
const changedStyleKeys = collectChangedStyleKeys(oldStyle, newStyle);
|
|
2840
|
+
const styleChanged = changedStyleKeys.length > 0;
|
|
2841
|
+
const transitionsChanged = !transitionsEqual(oldTransitions, newTransitions);
|
|
2842
|
+
const propsChanged = !propsEqual(oldCanonicalProps, newCanonicalProps);
|
|
2843
|
+
const textContentChanged =
|
|
2844
|
+
oldCanonicalProps.textContent !== newCanonicalProps.textContent ||
|
|
2845
|
+
oldCanonicalProps.value !== newCanonicalProps.value;
|
|
2846
|
+
const textMeasureChanged =
|
|
2847
|
+
instance.tagType === 'text' && oldParagraphKey !== nextState.paragraphKey;
|
|
2848
|
+
const inheritedStateChanged =
|
|
2849
|
+
oldResolvedDirection !== nextState.resolvedDirection ||
|
|
2850
|
+
oldResolvedWritingMode !== nextState.resolvedWritingMode ||
|
|
2851
|
+
oldResolvedLang !== nextState.resolvedLang ||
|
|
2852
|
+
!safeAreaPropagationEqual(oldPropagatedSafeArea, nextState.propagatedSafeArea);
|
|
2853
|
+
|
|
2854
|
+
applyResolvedElementState(instance, nextState);
|
|
2855
|
+
|
|
2856
|
+
// Update events if changed
|
|
2857
|
+
const eventsChanged = eventPropsChanged(previousProps, newProps);
|
|
2858
|
+
|
|
2859
|
+
if (eventsChanged) {
|
|
2860
|
+
processEventProps(instance, newProps, config);
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// Store the new original props for future diffing
|
|
2864
|
+
instance.originalProps = newProps;
|
|
2865
|
+
|
|
2866
|
+
if (hasDirtyFlag(instance, DirtyFlags.Created)) {
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
const enc = getEncoderForNode(instance);
|
|
2871
|
+
|
|
2872
|
+
if (styleChanged) {
|
|
2873
|
+
markViewChanged(instance);
|
|
2874
|
+
if (styleKeysAffectLayout(changedStyleKeys)) {
|
|
2875
|
+
markLayoutDirtyForNode(instance);
|
|
2876
|
+
}
|
|
2877
|
+
encodeStyleUpdate(enc, instance.id, changedStyleKeys, instance.style);
|
|
2878
|
+
if (instance.tagType === 'text' && styleKeysAffectTextLeafMeasure(changedStyleKeys)) {
|
|
2879
|
+
encodeTextLeafMeasureStyles(enc, instance);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
if (transitionsChanged) {
|
|
2884
|
+
markViewChanged(instance);
|
|
2885
|
+
setTransition(enc, instance.id, instance.transitions);
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
if (propsChanged) {
|
|
2889
|
+
markViewChanged(instance);
|
|
2890
|
+
getPendingCommitState(getRootIdForNode(instance)).warningAnalysisDirty = true;
|
|
2891
|
+
const canPatchStableTextContent =
|
|
2892
|
+
textContentChanged &&
|
|
2893
|
+
!styleChanged &&
|
|
2894
|
+
canPatchElementTextWithoutLayout(instance, newProps);
|
|
2895
|
+
if (
|
|
2896
|
+
(textMeasureChanged || textContentChanged) &&
|
|
2897
|
+
!canPatchStableTextContent
|
|
2898
|
+
) {
|
|
2899
|
+
markLayoutDirtyForNode(instance);
|
|
2900
|
+
}
|
|
2901
|
+
if (canPatchStableTextContent && typeof newCanonicalProps.textContent === 'string') {
|
|
2902
|
+
setTextContent(enc, instance.id, newCanonicalProps.textContent);
|
|
2903
|
+
} else {
|
|
2904
|
+
encodeProps(enc, instance);
|
|
2905
|
+
}
|
|
2906
|
+
} else if (
|
|
2907
|
+
oldResolvedLang !== instance.resolvedLang &&
|
|
2908
|
+
typeof instance.props.lang === 'string' &&
|
|
2909
|
+
instance.props.lang.length > 0
|
|
2910
|
+
) {
|
|
2911
|
+
markViewChanged(instance);
|
|
2912
|
+
setLang(enc, instance.id, instance.resolvedLang);
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
if (oldCanonicalProps.selectable !== newCanonicalProps.selectable) {
|
|
2916
|
+
for (const child of instance.children) {
|
|
2917
|
+
syncSelectablePropsInSubtree(enc, child);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
if (inheritedStateChanged) {
|
|
2922
|
+
for (const child of instance.children) {
|
|
2923
|
+
syncInheritedStateInSubtree(enc, child);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
if (eventsChanged) {
|
|
2928
|
+
markViewChanged(instance);
|
|
2929
|
+
getPendingCommitState(getRootIdForNode(instance)).warningAnalysisDirty = true;
|
|
2930
|
+
encodeEvents(enc, instance);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
/**
|
|
2935
|
+
* Update a text node's content and encode the change.
|
|
2936
|
+
*/
|
|
2937
|
+
export function updateTextContent(instance: TextNode, newText: string): void {
|
|
2938
|
+
instance.text = newText;
|
|
2939
|
+
if (hasDirtyFlag(instance, DirtyFlags.Created)) {
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2942
|
+
markViewChanged(instance);
|
|
2943
|
+
if (!canPatchTextNodeWithoutLayout(instance)) {
|
|
2944
|
+
markLayoutDirtyForNode(instance);
|
|
2945
|
+
}
|
|
2946
|
+
setTextContent(getEncoderForNode(instance), instance.id, newText);
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
/**
|
|
2950
|
+
* Finalize initial children after tree building.
|
|
2951
|
+
* Kept as a no-op because protocol encoding happens only once React commits placement.
|
|
2952
|
+
*/
|
|
2953
|
+
export function finalizeInstance(instance: ElementNode): void {
|
|
2954
|
+
void instance;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
/**
|
|
2958
|
+
* Commit the batch - set root styles, compute layout, and dispatch to native.
|
|
2959
|
+
*/
|
|
2960
|
+
export function commitBatch(container: RootNode): void {
|
|
2961
|
+
const enc = getEncoderForRoot(container.rootId);
|
|
2962
|
+
const state = getPendingCommitState(container.rootId);
|
|
2963
|
+
const rootChildrenDirty =
|
|
2964
|
+
hasDirtyFlag(container, DirtyFlags.Created) ||
|
|
2965
|
+
hasDirtyFlag(container, DirtyFlags.Children);
|
|
2966
|
+
if (rootChildrenDirty) {
|
|
2967
|
+
state.dirtyChildren.add(container);
|
|
2968
|
+
state.layoutDirty = true;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
flushDirtyChildren(enc, state);
|
|
2972
|
+
|
|
2973
|
+
const { width, height } = getScreenDimensions(container.rootId);
|
|
2974
|
+
const previousViewport = committedRootViewportSizes.get(container.rootId);
|
|
2975
|
+
const viewportChanged =
|
|
2976
|
+
previousViewport === undefined ||
|
|
2977
|
+
previousViewport.width !== width ||
|
|
2978
|
+
previousViewport.height !== height;
|
|
2979
|
+
|
|
2980
|
+
if (viewportChanged) {
|
|
2981
|
+
committedRootViewportSizes.set(container.rootId, { width, height });
|
|
2982
|
+
state.layoutDirty = true;
|
|
2983
|
+
state.changedViews.add(container.id);
|
|
2984
|
+
|
|
2985
|
+
setStyle(enc, container.id, {
|
|
2986
|
+
width: { type: 'points', value: width },
|
|
2987
|
+
height: { type: 'points', value: height },
|
|
2988
|
+
flexDirection: 'column',
|
|
2989
|
+
alignItems: 'stretch',
|
|
2990
|
+
justifyContent: 'flex-start',
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
if (state.layoutDirty) {
|
|
2995
|
+
enc.computeLayout(container.id, width, height);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
if (enc.getOpCount() > 0) {
|
|
2999
|
+
dispatchProtocol(enc);
|
|
3000
|
+
}
|
|
3001
|
+
const changedViews = Array.from(state.changedViews);
|
|
3002
|
+
const analyzeWarnings = state.warningAnalysisDirty;
|
|
3003
|
+
state.layoutDirty = false;
|
|
3004
|
+
state.warningAnalysisDirty = false;
|
|
3005
|
+
state.changedViews.clear();
|
|
3006
|
+
clearDirtyFlags(container);
|
|
3007
|
+
notifyCommit(container, {
|
|
3008
|
+
changedViews,
|
|
3009
|
+
analyzeWarnings,
|
|
3010
|
+
});
|
|
3011
|
+
syncKeyboardAvoidanceAfterCommit(container.rootId);
|
|
3012
|
+
// Plain-web pixels for host-ops adapters: when the DOM mirror is installed
|
|
3013
|
+
// (no protocol host on the page), it re-renders this root's node tree into
|
|
3014
|
+
// real DOM after every commit. Undefined everywhere else.
|
|
3015
|
+
(globalThis as typeof globalThis & {
|
|
3016
|
+
__exactDomMirror?: { sync(root: RootNode): void };
|
|
3017
|
+
}).__exactDomMirror?.sync(container);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
/**
|
|
3021
|
+
* Render a minimal fallback message when framework rendering fails.
|
|
3022
|
+
* This keeps the native tree in a coherent state instead of leaving the app blank.
|
|
3023
|
+
*/
|
|
3024
|
+
export function renderErrorFallback(container: RootNode, message: string): void {
|
|
3025
|
+
const enc = getEncoderForRoot(container.rootId);
|
|
3026
|
+
const fallbackProps: Props = {
|
|
3027
|
+
style: {
|
|
3028
|
+
color: '#ff3b30',
|
|
3029
|
+
padding: 16,
|
|
3030
|
+
fontSize: 14,
|
|
3031
|
+
},
|
|
3032
|
+
};
|
|
3033
|
+
|
|
3034
|
+
clearContainer(container);
|
|
3035
|
+
|
|
3036
|
+
const fallback = createInstance('Text', fallbackProps);
|
|
3037
|
+
encodeCreateElement(enc, fallback);
|
|
3038
|
+
|
|
3039
|
+
const text = createTextNode(message);
|
|
3040
|
+
encodeCreateText(enc, text);
|
|
3041
|
+
|
|
3042
|
+
nodeAppendChild(fallback, text);
|
|
3043
|
+
nodeAppendChild(container, fallback);
|
|
3044
|
+
encodeChildren(enc, fallback);
|
|
3045
|
+
encodeChildren(enc, container);
|
|
3046
|
+
commitBatch(container);
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* Create the root container and register it with native.
|
|
3051
|
+
*
|
|
3052
|
+
* @param rootId - Root ID for multi-root support (default 0 = single root)
|
|
3053
|
+
* @param owner - Optional public adapter owner name. When supplied, the root
|
|
3054
|
+
* ID is claimed so another adapter/DOM handle cannot silently reuse it.
|
|
3055
|
+
*/
|
|
3056
|
+
export function createRoot(rootId: number = 0, owner?: string): RootNode {
|
|
3057
|
+
if (liveRoots.has(rootId)) {
|
|
3058
|
+
throw new Error(`Root ${rootId} is already initialized.`);
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
if (owner) {
|
|
3062
|
+
claimRoot(rootId, owner);
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
const root = createRootNode(rootId);
|
|
3066
|
+
const enc = getEncoderForRoot(rootId);
|
|
3067
|
+
enc.createView(root.id, NodeType.View);
|
|
3068
|
+
liveRoots.set(root.rootId, root);
|
|
3069
|
+
ensureLocaleRootSync();
|
|
3070
|
+
ensureAccessibilityRootSync();
|
|
3071
|
+
registerRoot(root);
|
|
3072
|
+
|
|
3073
|
+
const ensureAgentServer = (
|
|
3074
|
+
globalThis as {
|
|
3075
|
+
__exactEnsureAgentServer?: (() => void) | undefined;
|
|
3076
|
+
}
|
|
3077
|
+
).__exactEnsureAgentServer;
|
|
3078
|
+
const shouldBootAgentServer =
|
|
3079
|
+
(globalThis as {
|
|
3080
|
+
__exactAgentBootstrap?: {
|
|
3081
|
+
boot?: boolean;
|
|
3082
|
+
};
|
|
3083
|
+
}).__exactAgentBootstrap?.boot === true ||
|
|
3084
|
+
(typeof process === 'object' &&
|
|
3085
|
+
process !== null &&
|
|
3086
|
+
typeof process.env?.EXACT_AGENT_BOOT === 'string' &&
|
|
3087
|
+
process.env.EXACT_AGENT_BOOT === '1');
|
|
3088
|
+
|
|
3089
|
+
if (shouldBootAgentServer && typeof ensureAgentServer === 'function') {
|
|
3090
|
+
ensureAgentServer();
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
return root;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
export function unregisterInspectorRoot(rootId: number): void {
|
|
3097
|
+
liveRoots.delete(rootId);
|
|
3098
|
+
committedRootViewportSizes.delete(rootId);
|
|
3099
|
+
pendingCommitStateByRoot.delete(rootId);
|
|
3100
|
+
maybeDisposeLocaleRootSync();
|
|
3101
|
+
maybeDisposeAccessibilityRootSync();
|
|
3102
|
+
unregisterRoot(rootId);
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
export function _resetHostOpsState(): void {
|
|
3106
|
+
liveRoots.clear();
|
|
3107
|
+
rootOwners.clear();
|
|
3108
|
+
committedRootViewportSizes.clear();
|
|
3109
|
+
pendingCommitStateByRoot.clear();
|
|
3110
|
+
maybeDisposeLocaleRootSync();
|
|
3111
|
+
maybeDisposeAccessibilityRootSync();
|
|
3112
|
+
encoders.clear();
|
|
3113
|
+
warnedAutoDirectionTags.clear();
|
|
3114
|
+
resetProtocolTapForTests();
|
|
3115
|
+
resetKeyboardAvoidanceState();
|
|
3116
|
+
clearFocusedTarget();
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// ---------------------------------------------------------------------------
|
|
3120
|
+
// LLP 0281 stage 0 — the host channel-application seam
|
|
3121
|
+
// ---------------------------------------------------------------------------
|
|
3122
|
+
//
|
|
3123
|
+
// A value-channel style sink (a `transform`/`opacity` attr driven by a
|
|
3124
|
+
// frame-coalesced channel value) must reach pixels as compositor-only work,
|
|
3125
|
+
// bypassing the reactive flush and the host-ops prop diff entirely. Hosts
|
|
3126
|
+
// that can do that register an applier here (the web dom-mirror writes the
|
|
3127
|
+
// style straight onto the DOM node); the Contract runtime calls
|
|
3128
|
+
// `applyHostChannelStyle` per coalesced frame and falls back to an ordinary
|
|
3129
|
+
// coalesced prop update when no applier is installed (correct semantics,
|
|
3130
|
+
// slower path — the native fast path arrives with LLP 0099's substrate).
|
|
3131
|
+
|
|
3132
|
+
export type HostChannelStyleApplier = (
|
|
3133
|
+
/** The host-ops element node the sink is bound to. */
|
|
3134
|
+
node: unknown,
|
|
3135
|
+
/** Lowered style prop name (`transform`, `opacity`). */
|
|
3136
|
+
styleProp: string,
|
|
3137
|
+
/** Evaluated sink value (string transform, numeric opacity, ...). */
|
|
3138
|
+
value: unknown,
|
|
3139
|
+
) => boolean;
|
|
3140
|
+
|
|
3141
|
+
let hostChannelStyleApplier: HostChannelStyleApplier | null = null;
|
|
3142
|
+
|
|
3143
|
+
/** Install (or clear, with null) the host's channel style applier. */
|
|
3144
|
+
export function setHostChannelStyleApplier(next: HostChannelStyleApplier | null): void {
|
|
3145
|
+
hostChannelStyleApplier = next;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
export function getHostChannelStyleApplier(): HostChannelStyleApplier | null {
|
|
3149
|
+
return hostChannelStyleApplier;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
/**
|
|
3153
|
+
* Apply a channel-driven style value through the host fast path. Returns
|
|
3154
|
+
* false when no host applier is installed or the applier declined the node
|
|
3155
|
+
* — callers then fall back to the coalesced host-ops update.
|
|
3156
|
+
*/
|
|
3157
|
+
export function applyHostChannelStyle(node: unknown, styleProp: string, value: unknown): boolean {
|
|
3158
|
+
if (!hostChannelStyleApplier) {
|
|
3159
|
+
return false;
|
|
3160
|
+
}
|
|
3161
|
+
try {
|
|
3162
|
+
return hostChannelStyleApplier(node, styleProp, value);
|
|
3163
|
+
} catch (error) {
|
|
3164
|
+
console.warn('[exact] host channel style applier failed', error);
|
|
3165
|
+
return false;
|
|
3166
|
+
}
|
|
3167
|
+
}
|