@symbiote-native/engine 0.1.7 → 0.3.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/README.md +17 -10
- package/build/action-sheet-ios/index.js +4 -1
- package/build/alert/index.ios.js +10 -0
- package/build/animated/animations/composition.js +3 -1
- package/build/animated/animations/spring-config.js +4 -1
- package/build/animated/animations/spring.js +23 -9
- package/build/animated/animations/timing.js +3 -2
- package/build/animated/bezier.js +2 -1
- package/build/animated/color.d.ts +2 -8
- package/build/animated/color.js +8 -47
- package/build/animated/easing.js +0 -8
- package/build/animated/event.js +7 -3
- package/build/animated/graph.d.ts +13 -5
- package/build/animated/graph.js +92 -13
- package/build/animated/index.d.ts +3 -3
- package/build/animated/index.js +3 -3
- package/build/animated/interpolation.js +8 -3
- package/build/animated/leaf-lifecycle.d.ts +7 -0
- package/build/animated/leaf-lifecycle.js +185 -0
- package/build/animated/mock.js +1 -1
- package/build/animated/native/native-animated.js +2 -1
- package/build/animated/operators.js +27 -6
- package/build/animated/props.js +1 -1
- package/build/animated/rgba.d.ts +8 -0
- package/build/animated/rgba.js +70 -0
- package/build/animated/style.js +13 -3
- package/build/animated/value-xy.d.ts +8 -5
- package/build/animated/value-xy.js +4 -1
- package/build/animated/value.js +12 -7
- package/build/appearance/index.js +4 -1
- package/build/commit.d.ts +13 -0
- package/build/commit.js +146 -14
- package/build/debug.d.ts +1 -1
- package/build/debug.js +10 -3
- package/build/events/index.js +19 -10
- package/build/fabric-props.js +8 -2
- package/build/image-loader.js +3 -2
- package/build/index.d.ts +23 -19
- package/build/index.js +19 -11
- package/build/interaction-manager/index.js +2 -1
- package/build/layout-animation/index.js +12 -5
- package/build/native-events.js +2 -1
- package/build/native-modules/index.js +3 -1
- package/build/node.d.ts +25 -0
- package/build/node.js +168 -2
- package/build/pan-responder/index.js +20 -9
- package/build/permissions-android/index.js +3 -1
- package/build/platform/index.android.d.ts +1 -1
- package/build/platform/index.android.js +1 -1
- package/build/platform/index.ios.d.ts +1 -1
- package/build/platform-color/index.js +3 -1
- package/build/post-commit.d.ts +1 -0
- package/build/post-commit.js +6 -0
- package/build/process-background-image/index.js +30 -9
- package/build/process-filter.js +11 -4
- package/build/process-font-variant.js +0 -1
- package/build/process-transform/index.js +13 -19
- package/build/registry.js +10 -2
- package/build/report-error.d.ts +5 -0
- package/build/report-error.js +63 -0
- package/build/share/index.android.js +1 -1
- package/build/share/index.ios.js +1 -1
- package/build/status-bar/index.android.js +8 -10
- package/build/status-bar/index.ios.js +1 -1
- package/build/style-registry/index.d.ts +9 -2
- package/build/style-registry/index.js +149 -95
- package/build/style-registry/scope.d.ts +15 -1
- package/build/style-registry/scope.js +29 -27
- package/build/style-sheet/index.js +4 -2
- package/build/styles.d.ts +11 -1
- package/build/surface.d.ts +1 -1
- package/build/surface.js +7 -1
- package/build/tags.d.ts +1 -0
- package/build/tags.js +31 -1
- package/build/touch-history.js +9 -2
- package/build/vibration/index.android.js +2 -3
- package/build/vibration/index.ios.js +1 -1
- package/build/view-config.js +14 -2
- package/package.json +2 -2
- package/build/animated/interpolation-node.d.ts +0 -15
- package/build/animated/interpolation-node.js +0 -58
- package/build/style-registry/scope/index.d.ts +0 -3
- package/build/style-registry/scope/index.js +0 -41
- package/build/touch-history/index.d.ts +0 -23
- package/build/touch-history/index.js +0 -150
- package/build/type-guards/index.d.ts +0 -4
- package/build/type-guards/index.js +0 -15
|
@@ -42,7 +42,7 @@ const ANIMATION_PROPERTY = {
|
|
|
42
42
|
// (`FabricUIManager?.configureNextLayoutAnimation` in LayoutAnimation.js) before
|
|
43
43
|
// calling it, no `as` cast needed.
|
|
44
44
|
function hasConfigureNextLayoutAnimation(value) {
|
|
45
|
-
return isRecord(value) && typeof value.configureNextLayoutAnimation === 'function';
|
|
45
|
+
return (isRecord(value) && typeof value.configureNextLayoutAnimation === 'function');
|
|
46
46
|
}
|
|
47
47
|
// Resolved FRESH on every configureNext, deliberately NOT memoized. Either mechanism can
|
|
48
48
|
// come and go at runtime (the Fabric global installs during bootstrap; a TurboModule can be
|
|
@@ -93,9 +93,15 @@ const Presets = {
|
|
|
93
93
|
linear: createLayoutAnimation(PRESET_DURATION.linear, ANIMATION_TYPE.linear, ANIMATION_PROPERTY.opacity),
|
|
94
94
|
spring: {
|
|
95
95
|
duration: PRESET_DURATION.spring,
|
|
96
|
-
create: {
|
|
96
|
+
create: {
|
|
97
|
+
type: ANIMATION_TYPE.linear,
|
|
98
|
+
property: ANIMATION_PROPERTY.opacity,
|
|
99
|
+
},
|
|
97
100
|
update: { type: ANIMATION_TYPE.spring, springDamping: SPRING_DAMPING },
|
|
98
|
-
delete: {
|
|
101
|
+
delete: {
|
|
102
|
+
type: ANIMATION_TYPE.linear,
|
|
103
|
+
property: ANIMATION_PROPERTY.opacity,
|
|
104
|
+
},
|
|
99
105
|
},
|
|
100
106
|
};
|
|
101
107
|
// ---- enabled gate --------------------------------------------------------
|
|
@@ -148,14 +154,15 @@ function configureNext(config, onAnimationDidEnd, onAnimationDidFail) {
|
|
|
148
154
|
onAnimationDidEnd?.();
|
|
149
155
|
};
|
|
150
156
|
dlog(`LayoutAnimation.configureNext: dispatching config (duration=${config.duration})`);
|
|
151
|
-
// Default to a no-op when the caller doesn't supply one.
|
|
152
157
|
manager.configureNextLayoutAnimation(config, onComplete, onAnimationDidFail ?? (() => { }));
|
|
153
158
|
}
|
|
154
159
|
// ---- the LayoutAnimation facade ------------------------------------------
|
|
155
160
|
class LayoutAnimationImpl {
|
|
156
161
|
// Frozen so callers can't mutate the shared type/property tables.
|
|
157
162
|
Types = Object.freeze({ ...ANIMATION_TYPE });
|
|
158
|
-
Properties = Object.freeze({
|
|
163
|
+
Properties = Object.freeze({
|
|
164
|
+
...ANIMATION_PROPERTY,
|
|
165
|
+
});
|
|
159
166
|
Presets = Presets;
|
|
160
167
|
// Methods (not class fields) so they stay overridable under
|
|
161
168
|
// useDefineForClassFields.
|
package/build/native-events.js
CHANGED
|
@@ -70,7 +70,8 @@ export function setDeviceEventSource(source) {
|
|
|
70
70
|
// would throw "undefined is not a function" — the `?.` guards the module, not a
|
|
71
71
|
// missing method. Mirrors RN's NativeEventEmitter constructor probe.
|
|
72
72
|
function hasObserveCounters(module) {
|
|
73
|
-
return typeof module.addListener === 'function' &&
|
|
73
|
+
return (typeof module.addListener === 'function' &&
|
|
74
|
+
typeof module.removeListeners === 'function');
|
|
74
75
|
}
|
|
75
76
|
// Subscribe to events for one native module. Mirrors RN's NativeEventEmitter: each
|
|
76
77
|
// `addListener` also pings the module's `addListener` counter, and removal pings
|
|
@@ -110,7 +110,9 @@ export function createDeviceEventModule(config) {
|
|
|
110
110
|
installDeviceEventHub();
|
|
111
111
|
const resolved = getModule();
|
|
112
112
|
const bindModule = config.bindModuleToEmitter ?? true;
|
|
113
|
-
const boundModule = bindModule && resolved !== null && hasEventEmitterShape(resolved)
|
|
113
|
+
const boundModule = bindModule && resolved !== null && hasEventEmitterShape(resolved)
|
|
114
|
+
? resolved
|
|
115
|
+
: undefined;
|
|
114
116
|
emitter = new NativeEventEmitter(boundModule);
|
|
115
117
|
config.onEmitterCreated?.(emitter, resolved);
|
|
116
118
|
}
|
package/build/node.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface ISymbioteNode {
|
|
|
19
19
|
listeners: Map<string, IListener> | undefined;
|
|
20
20
|
children: ISymbioteNode[];
|
|
21
21
|
parent: ISymbioteNode | undefined;
|
|
22
|
+
dirty: boolean;
|
|
22
23
|
}
|
|
23
24
|
export declare function createElement(component: string, isText?: boolean): ISymbioteNode;
|
|
24
25
|
export declare function createRawText(text: string): ISymbioteNode;
|
|
@@ -27,12 +28,36 @@ export declare function debugNodeId(node: ISymbioteNode): number;
|
|
|
27
28
|
export declare const ANCHOR_COMPONENT = "#anchor";
|
|
28
29
|
export declare function createAnchor(): ISymbioteNode;
|
|
29
30
|
export declare function isAnchor(node: ISymbioteNode): boolean;
|
|
31
|
+
export declare function isEmptyRawText(node: ISymbioteNode): boolean;
|
|
32
|
+
export declare function markDirty(node: ISymbioteNode): void;
|
|
33
|
+
export declare function takePropStats(): {
|
|
34
|
+
writes: number;
|
|
35
|
+
noops: number;
|
|
36
|
+
};
|
|
30
37
|
export declare function setProp(node: ISymbioteNode, key: string, value: unknown): void;
|
|
31
38
|
export declare function setEventListener(node: ISymbioteNode, name: string, value: unknown): void;
|
|
39
|
+
/**
|
|
40
|
+
* Stop a node painting without unmounting it, or let it paint again.
|
|
41
|
+
*
|
|
42
|
+
* The seam React's `Activity`/`Suspense` reach for through `hideInstance`/`unhideInstance`. It
|
|
43
|
+
* lives in the engine rather than an adapter because the reversibility problem — restoring the
|
|
44
|
+
* author's style byte for byte — belongs to whoever owns the style merge, and that is here.
|
|
45
|
+
*/
|
|
46
|
+
export declare function setNodeHidden(node: ISymbioteNode, hidden: boolean): void;
|
|
32
47
|
export declare function getExplicitStyle(node: ISymbioteNode): unknown;
|
|
33
48
|
export declare function routeProp(node: ISymbioteNode, key: string, value: unknown): void;
|
|
34
49
|
export declare function setText(node: ISymbioteNode, text: string): void;
|
|
35
50
|
export declare function appendChild(parent: ISymbioteNode, child: ISymbioteNode): void;
|
|
36
51
|
export declare function insertBefore(parent: ISymbioteNode, child: ISymbioteNode, beforeChild: ISymbioteNode): void;
|
|
37
52
|
export declare function removeChild(parent: ISymbioteNode, child: ISymbioteNode): void;
|
|
53
|
+
export interface ITreeCensus {
|
|
54
|
+
nodes: number;
|
|
55
|
+
anchors: number;
|
|
56
|
+
emptyRawTexts: number;
|
|
57
|
+
/** Nodes the commit walk actually reconciles: `nodes` minus everything it skips. */
|
|
58
|
+
renderable: number;
|
|
59
|
+
/** children.length of every parent holding at least one skipped child, widest first. */
|
|
60
|
+
flattenWidths: number[];
|
|
61
|
+
}
|
|
62
|
+
export declare function censusRetainedTree(roots: readonly ISymbioteNode[]): ITreeCensus;
|
|
38
63
|
export {};
|
package/build/node.js
CHANGED
|
@@ -34,6 +34,7 @@ export function createElement(component, isText = false) {
|
|
|
34
34
|
listeners: undefined,
|
|
35
35
|
children: [],
|
|
36
36
|
parent: undefined,
|
|
37
|
+
dirty: true,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
export function createRawText(text) {
|
|
@@ -45,6 +46,7 @@ export function createRawText(text) {
|
|
|
45
46
|
listeners: undefined,
|
|
46
47
|
children: [],
|
|
47
48
|
parent: undefined,
|
|
49
|
+
dirty: true,
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
// `instanceHandle` round-trips through Fabric unchanged: the object we pass to
|
|
@@ -80,16 +82,97 @@ export function createAnchor() {
|
|
|
80
82
|
export function isAnchor(node) {
|
|
81
83
|
return node.component === ANCHOR_COMPONENT;
|
|
82
84
|
}
|
|
85
|
+
// A raw text with no characters must not reach Fabric. Its fragment is dropped by
|
|
86
|
+
// AttributedString::appendFragment, but the text walk has already flagged "the last child was raw
|
|
87
|
+
// text", so the NEXT raw sibling merges into `fragments.back()` of an empty vector and the process
|
|
88
|
+
// aborts. The commit walk skips such a node exactly as it skips an anchor (commit.ts,
|
|
89
|
+
// renderableChildren); an empty string paints nothing either way, so nothing is lost. `''` only —
|
|
90
|
+
// a whitespace-only string is real content inside a <Text>.
|
|
91
|
+
export function isEmptyRawText(node) {
|
|
92
|
+
return node.component === RAW_TEXT_COMPONENT && node.props.text === '';
|
|
93
|
+
}
|
|
94
|
+
// Dirty-marking: lets reconcile return an untouched subtree by reference instead of rebuilding
|
|
95
|
+
// every node's Fabric props and deep-comparing them against the mirror. That walk costs ~13 us per
|
|
96
|
+
// node on device (Hermes, iOS Debug, examples/react benchmark screen), so without the flag a
|
|
97
|
+
// 1200-node tree burns a whole 16.6 ms frame no matter how small the change - the cost tracks TREE
|
|
98
|
+
// SIZE, not change size.
|
|
99
|
+
//
|
|
100
|
+
// Marking walks up to the first ALREADY-dirty ancestor and stops, so a burst of mutations under one
|
|
101
|
+
// subtree pays for one chain walk rather than one per mutation. Reconcile clears every node it
|
|
102
|
+
// visits, which keeps the invariant "an ancestor of a dirty node is dirty" across commits.
|
|
103
|
+
//
|
|
104
|
+
// Listener changes deliberately do NOT mark. `node.listeners` never reaches Fabric (event dispatch
|
|
105
|
+
// reads it straight off the retained node) and React hands us a fresh handler closure on nearly
|
|
106
|
+
// every render, so marking there would re-dirty the whole tree every commit and hand the win back.
|
|
107
|
+
// The one listener that DOES change a Fabric prop, `layout`, raises `onLayout` through setProp
|
|
108
|
+
// below and is marked that way.
|
|
109
|
+
export function markDirty(node) {
|
|
110
|
+
let current = node;
|
|
111
|
+
while (current !== undefined && !current.dirty) {
|
|
112
|
+
current.dirty = true;
|
|
113
|
+
current = current.parent;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// How many prop writes actually landed, and how many the no-op guard below turned away.
|
|
117
|
+
// Read-and-zeroed through readCommitProfile() (commit.ts), which folds them into the same window
|
|
118
|
+
// as the walk numbers so one read prices both halves: `propNoops` is the waste an adapter is
|
|
119
|
+
// generating above the engine, `nodesVisited` is what that waste costs below it.
|
|
120
|
+
//
|
|
121
|
+
// Not gated behind isDebug(), for the same reason the commit profile is not: an integer increment
|
|
122
|
+
// is noise next to the prop write it counts, and the figure is only meaningful from a release
|
|
123
|
+
// build. A per-call dlog was the obvious alternative and is deliberately NOT here - the Angular
|
|
124
|
+
// screen that motivated the guard emitted 90 000 no-op writes on one press, and a log line each
|
|
125
|
+
// would measure the logging rather than the code (see the `perf-claims-need-numbers` rule).
|
|
126
|
+
const propStats = { writes: 0, noops: 0 };
|
|
127
|
+
export function takePropStats() {
|
|
128
|
+
const snapshot = { writes: propStats.writes, noops: propStats.noops };
|
|
129
|
+
propStats.writes = 0;
|
|
130
|
+
propStats.noops = 0;
|
|
131
|
+
return snapshot;
|
|
132
|
+
}
|
|
83
133
|
// A pure prop set: no event inference. `onTintColor` is a Switch prop and reaches
|
|
84
134
|
// Fabric like any other; the event-vs-prop decision is made by routeProp, never by
|
|
85
135
|
// the key's name.
|
|
136
|
+
//
|
|
137
|
+
// Writing a value the node already holds is a NO-OP and returns before markDirty. Fabric never saw
|
|
138
|
+
// a difference either way - reconcile rebuilds the node's Fabric props and `propsEqual` finds them
|
|
139
|
+
// identical, so no clone is emitted - but the mark itself is not free: it walks to the first
|
|
140
|
+
// already-dirty ancestor and strips every one of them of the commit walk's early exit, so an
|
|
141
|
+
// otherwise untouched subtree gets re-walked purely to prove it is untouched. The guard makes that
|
|
142
|
+
// whole bug class free for every adapter instead of each one having to remember to diff first
|
|
143
|
+
// (measured: Angular's Pressable host bag pushed 104 000 setProp calls for a screen Solid built in
|
|
144
|
+
// 12 000, 90 000 of them writing `undefined` over a key that was not there).
|
|
145
|
+
//
|
|
146
|
+
// Three deliberate choices:
|
|
147
|
+
//
|
|
148
|
+
// - `Object.hasOwn`, not `node.props[key] === undefined`. A key explicitly present holding
|
|
149
|
+
// `undefined` is not an absent key: `delete` genuinely changes the record's shape, and
|
|
150
|
+
// `setNativeProps` writes node.props directly and can leave exactly such a key behind. Fabric
|
|
151
|
+
// itself cannot tell the two apart (fabricProps skips undefined values), but node.props is also
|
|
152
|
+
// read outside the commit path, so the retained tree keeps the shape callers asked for.
|
|
153
|
+
// - `Object.is`, not a deep compare. A style object, an array, or a handler closure is a fresh
|
|
154
|
+
// reference on nearly every render, so the guard simply never fires for them - correct, since an
|
|
155
|
+
// adapter is free to hand back the SAME reference with mutated contents and identity cannot see
|
|
156
|
+
// that. A deep compare on every prop write would cost more than the walk it saves.
|
|
157
|
+
// - The in-place-mutation hazard that leaves is already instrumented: a node skipped as clean whose
|
|
158
|
+
// props have drifted is exactly what `warnIfStale` reports as DIRTY-MISS under DEBUG (commit.ts).
|
|
86
159
|
export function setProp(node, key, value) {
|
|
87
160
|
if (value === undefined) {
|
|
161
|
+
if (!Object.hasOwn(node.props, key)) {
|
|
162
|
+
propStats.noops += 1;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
88
165
|
delete node.props[key];
|
|
89
166
|
}
|
|
90
167
|
else {
|
|
168
|
+
if (Object.is(node.props[key], value)) {
|
|
169
|
+
propStats.noops += 1;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
91
172
|
node.props[key] = value;
|
|
92
173
|
}
|
|
174
|
+
propStats.writes += 1;
|
|
175
|
+
markDirty(node);
|
|
93
176
|
}
|
|
94
177
|
// Fabric gates layout events behind a boolean prop (BaseViewProps.onLayout): unlike
|
|
95
178
|
// scroll / touch / change, which the native component emits unconditionally, a
|
|
@@ -148,12 +231,41 @@ const RESPONDER_EVENTS = new Set([
|
|
|
148
231
|
// dynamic" (the instance holds functions) and the surface paints black, while iOS silently
|
|
149
232
|
// drops it. SFC/template authoring never produces them. Strip them here, once, so no
|
|
150
233
|
// adapter leaks React JSX dev metadata to the host, mirroring React's host config.
|
|
151
|
-
const REACT_JSX_DEV_PROPS = new Set([
|
|
234
|
+
const REACT_JSX_DEV_PROPS = new Set([
|
|
235
|
+
'__self',
|
|
236
|
+
'__source',
|
|
237
|
+
]);
|
|
152
238
|
const classStyleParts = new WeakMap();
|
|
239
|
+
// The fresh array below means setProp's no-op guard never fires for `style`, and that is
|
|
240
|
+
// DELIBERATE - do not "finish the optimization" by skipping when both halves are unchanged.
|
|
241
|
+
// `classStyleParts` is a shadow copy of the declarative style, and setNativeProps bypasses it
|
|
242
|
+
// (it writes node.props.style directly, merging an Animated frame onto whatever is there). An
|
|
243
|
+
// app that hands over a hoisted style constant - StyleSheet.create, a module-level object - would
|
|
244
|
+
// then re-push an identity-equal half, get skipped, and never restore the declarative style the
|
|
245
|
+
// animation overwrote. The re-push IS the restore path.
|
|
153
246
|
function commitClassStyle(node, patch) {
|
|
154
247
|
const entry = { ...classStyleParts.get(node), ...patch };
|
|
155
248
|
classStyleParts.set(node, entry);
|
|
156
|
-
|
|
249
|
+
// The third slot is APPENDED ONLY WHILE HIDDEN. Writing a permanent three-element array would
|
|
250
|
+
// change the style payload of every node in every app for a state almost none of them are ever
|
|
251
|
+
// in — and this project spent a day removing per-frame allocations, so a slot that is undefined
|
|
252
|
+
// 99.9% of the time does not get to ride along on every style write.
|
|
253
|
+
setProp(node, 'style', entry.hiddenStyle === undefined
|
|
254
|
+
? [entry.classStyle, entry.explicitStyle]
|
|
255
|
+
: [entry.classStyle, entry.explicitStyle, entry.hiddenStyle]);
|
|
256
|
+
}
|
|
257
|
+
// `display: 'none'` is a real RN style value (Yoga's DisplayNone), so a hidden node keeps its
|
|
258
|
+
// place in the tree, its state and its children — it just stops laying out and painting.
|
|
259
|
+
const HIDDEN_STYLE = { display: 'none' };
|
|
260
|
+
/**
|
|
261
|
+
* Stop a node painting without unmounting it, or let it paint again.
|
|
262
|
+
*
|
|
263
|
+
* The seam React's `Activity`/`Suspense` reach for through `hideInstance`/`unhideInstance`. It
|
|
264
|
+
* lives in the engine rather than an adapter because the reversibility problem — restoring the
|
|
265
|
+
* author's style byte for byte — belongs to whoever owns the style merge, and that is here.
|
|
266
|
+
*/
|
|
267
|
+
export function setNodeHidden(node, hidden) {
|
|
268
|
+
commitClassStyle(node, { hiddenStyle: hidden ? HIDDEN_STYLE : undefined });
|
|
157
269
|
}
|
|
158
270
|
// The explicit (non-class-derived) style half, for an adapter that builds its style prop up
|
|
159
271
|
// key-by-key (Angular's Ivy ɵɵstyleProp/setStyle) instead of handing over one whole object —
|
|
@@ -199,9 +311,29 @@ export function routeProp(node, key, value) {
|
|
|
199
311
|
}
|
|
200
312
|
setProp(node, key, value);
|
|
201
313
|
}
|
|
314
|
+
// The same no-op guard as setProp, and here it is strictly stronger: `text` is a string, so
|
|
315
|
+
// `Object.is` is a real value comparison rather than the reference check it degrades to for a style
|
|
316
|
+
// object or a handler. A framework that re-renders a subtree and hands back an unchanged label -
|
|
317
|
+
// every list row whose text did not move, on every update - stops stripping its ancestors of the
|
|
318
|
+
// commit walk's early exit. Counted in the same propStats, because a text write IS a prop write:
|
|
319
|
+
// it lands in node.props.text and reaches Fabric as RCTRawText's only prop.
|
|
320
|
+
//
|
|
321
|
+
// Safe against the one ordering hazard worth naming: a raw-text node REPARENTED under a <Text>
|
|
322
|
+
// commits as RCTVirtualText instead of RCTText (viewNameFor, commit.ts). That flip is not driven
|
|
323
|
+
// from here - a structural op marks the parent chain, and reconcile re-checks `committed.parent` on
|
|
324
|
+
// its early-exit path - so a same-text write not marking cannot hide it.
|
|
202
325
|
export function setText(node, text) {
|
|
326
|
+
if (Object.is(node.props.text, text)) {
|
|
327
|
+
propStats.noops += 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
203
330
|
node.props.text = text;
|
|
331
|
+
propStats.writes += 1;
|
|
332
|
+
markDirty(node);
|
|
204
333
|
}
|
|
334
|
+
// Structural ops mark the PARENT chain (both the old and the new one), never the moved child:
|
|
335
|
+
// a child that only changed position may legitimately still be clean, and reconcile re-checks
|
|
336
|
+
// `committed.parent` on its early-exit path, so a reparent is caught there rather than by a flag.
|
|
205
337
|
function detach(child) {
|
|
206
338
|
const parent = child.parent;
|
|
207
339
|
if (!parent)
|
|
@@ -210,21 +342,55 @@ function detach(child) {
|
|
|
210
342
|
if (index >= 0)
|
|
211
343
|
parent.children.splice(index, 1);
|
|
212
344
|
child.parent = undefined;
|
|
345
|
+
markDirty(parent);
|
|
213
346
|
}
|
|
214
347
|
export function appendChild(parent, child) {
|
|
215
348
|
detach(child);
|
|
216
349
|
child.parent = parent;
|
|
217
350
|
parent.children.push(child);
|
|
351
|
+
markDirty(parent);
|
|
218
352
|
}
|
|
219
353
|
export function insertBefore(parent, child, beforeChild) {
|
|
220
354
|
detach(child);
|
|
221
355
|
child.parent = parent;
|
|
222
356
|
const index = parent.children.indexOf(beforeChild);
|
|
223
357
|
parent.children.splice(index < 0 ? parent.children.length : index, 0, child);
|
|
358
|
+
markDirty(parent);
|
|
224
359
|
}
|
|
225
360
|
export function removeChild(parent, child) {
|
|
226
361
|
const index = parent.children.indexOf(child);
|
|
227
362
|
if (index >= 0)
|
|
228
363
|
parent.children.splice(index, 1);
|
|
229
364
|
child.parent = undefined;
|
|
365
|
+
markDirty(parent);
|
|
366
|
+
}
|
|
367
|
+
export function censusRetainedTree(roots) {
|
|
368
|
+
const census = {
|
|
369
|
+
nodes: 0,
|
|
370
|
+
anchors: 0,
|
|
371
|
+
emptyRawTexts: 0,
|
|
372
|
+
renderable: 0,
|
|
373
|
+
flattenWidths: [],
|
|
374
|
+
};
|
|
375
|
+
// Explicit stack, not recursion: a deep list under a benchmark screen would blow the JS stack
|
|
376
|
+
// on the very tree this is meant to measure.
|
|
377
|
+
const stack = [...roots];
|
|
378
|
+
while (stack.length > 0) {
|
|
379
|
+
const node = stack.pop();
|
|
380
|
+
if (node === undefined)
|
|
381
|
+
break;
|
|
382
|
+
census.nodes += 1;
|
|
383
|
+
if (isAnchor(node))
|
|
384
|
+
census.anchors += 1;
|
|
385
|
+
else if (isEmptyRawText(node))
|
|
386
|
+
census.emptyRawTexts += 1;
|
|
387
|
+
else
|
|
388
|
+
census.renderable += 1;
|
|
389
|
+
if (node.children.some(child => isAnchor(child) || isEmptyRawText(child)))
|
|
390
|
+
census.flattenWidths.push(node.children.length);
|
|
391
|
+
for (const child of node.children)
|
|
392
|
+
stack.push(child);
|
|
393
|
+
}
|
|
394
|
+
census.flattenWidths.sort((left, right) => right - left);
|
|
395
|
+
return census;
|
|
230
396
|
}
|
|
@@ -17,7 +17,9 @@ const SINGLE_TOUCH_COUNT = 1;
|
|
|
17
17
|
// onShouldBlockNativeResponder defaults to true (RN: block native by default).
|
|
18
18
|
const DEFAULT_BLOCK_NATIVE_RESPONDER = true;
|
|
19
19
|
function toFiniteNumber(value) {
|
|
20
|
-
return typeof value === 'number' && Number.isFinite(value)
|
|
20
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
21
|
+
? value
|
|
22
|
+
: undefined;
|
|
21
23
|
}
|
|
22
24
|
// Pull one touch out of an untyped array element, skipping anything that does not
|
|
23
25
|
// carry numeric pageX/pageY. `timestamp` falls back to 0 when absent so a touch
|
|
@@ -196,7 +198,7 @@ function initializeGestureState(gestureState) {
|
|
|
196
198
|
// The timestamp this frame advances to: the touch-history clock when shared attached a
|
|
197
199
|
// store, else the most recent of the live touches (headless direct-call path).
|
|
198
200
|
function frameTimestampOf(event, touches) {
|
|
199
|
-
return touchHistoryOf(event)?.mostRecentTimeStamp ?? mostRecentTimestamp(touches);
|
|
201
|
+
return (touchHistoryOf(event)?.mostRecentTimeStamp ?? mostRecentTimestamp(touches));
|
|
200
202
|
}
|
|
201
203
|
// Advance the gesture for a move frame. With a touch-history store (the device / shared
|
|
202
204
|
// path) this defers to RN's accumulate-per-moved-touch math; without one it falls back to
|
|
@@ -275,7 +277,8 @@ const PanResponder = {
|
|
|
275
277
|
// Skip a duplicate dispatch of the same frame: when two touches change at
|
|
276
278
|
// once the responder system fires twice, but the geometry was already
|
|
277
279
|
// folded in on the first call.
|
|
278
|
-
if (gestureState._accountsForMovesUpTo ===
|
|
280
|
+
if (gestureState._accountsForMovesUpTo ===
|
|
281
|
+
frameTimestampOf(event, touches)) {
|
|
279
282
|
return false;
|
|
280
283
|
}
|
|
281
284
|
updateGestureStateOnMove(gestureState, event, touches);
|
|
@@ -288,14 +291,19 @@ const PanResponder = {
|
|
|
288
291
|
const touches = readTouches(event);
|
|
289
292
|
const touchHistory = touchHistoryOf(event);
|
|
290
293
|
// x0/y0 is the non-cumulative centroid at grant time (RN: currentCentroid).
|
|
291
|
-
gestureState.x0 = touchHistory
|
|
292
|
-
|
|
294
|
+
gestureState.x0 = touchHistory
|
|
295
|
+
? currentCentroidXAll(touchHistory)
|
|
296
|
+
: centroidX(touches);
|
|
297
|
+
gestureState.y0 = touchHistory
|
|
298
|
+
? currentCentroidYAll(touchHistory)
|
|
299
|
+
: centroidY(touches);
|
|
293
300
|
gestureState.dx = 0;
|
|
294
301
|
gestureState.dy = 0;
|
|
295
302
|
// The grant frame is already accounted for, so the first move's velocity
|
|
296
303
|
// is measured from here, not from time 0.
|
|
297
304
|
gestureState._accountsForMovesUpTo = frameTimestampOf(event, touches);
|
|
298
|
-
gestureState.numberActiveTouches =
|
|
305
|
+
gestureState.numberActiveTouches =
|
|
306
|
+
touchHistory?.numberActiveTouches ?? touches.length;
|
|
299
307
|
config.onPanResponderGrant?.(event, gestureState);
|
|
300
308
|
return config.onShouldBlockNativeResponder === undefined
|
|
301
309
|
? DEFAULT_BLOCK_NATIVE_RESPONDER
|
|
@@ -306,13 +314,15 @@ const PanResponder = {
|
|
|
306
314
|
},
|
|
307
315
|
onResponderStart(event) {
|
|
308
316
|
gestureState.numberActiveTouches =
|
|
309
|
-
touchHistoryOf(event)?.numberActiveTouches ??
|
|
317
|
+
touchHistoryOf(event)?.numberActiveTouches ??
|
|
318
|
+
readTouches(event).length;
|
|
310
319
|
config.onPanResponderStart?.(event, gestureState);
|
|
311
320
|
},
|
|
312
321
|
onResponderMove(event) {
|
|
313
322
|
const touches = readTouches(event);
|
|
314
323
|
// Same duplicate-frame guard as the capture path.
|
|
315
|
-
if (gestureState._accountsForMovesUpTo ===
|
|
324
|
+
if (gestureState._accountsForMovesUpTo ===
|
|
325
|
+
frameTimestampOf(event, touches)) {
|
|
316
326
|
return;
|
|
317
327
|
}
|
|
318
328
|
updateGestureStateOnMove(gestureState, event, touches);
|
|
@@ -320,7 +330,8 @@ const PanResponder = {
|
|
|
320
330
|
},
|
|
321
331
|
onResponderEnd(event) {
|
|
322
332
|
gestureState.numberActiveTouches =
|
|
323
|
-
touchHistoryOf(event)?.numberActiveTouches ??
|
|
333
|
+
touchHistoryOf(event)?.numberActiveTouches ??
|
|
334
|
+
readTouches(event).length;
|
|
324
335
|
config.onPanResponderEnd?.(event, gestureState);
|
|
325
336
|
},
|
|
326
337
|
onResponderRelease(event) {
|
|
@@ -84,7 +84,9 @@ export const PERMISSIONS = Object.freeze({
|
|
|
84
84
|
// string is passed through as a IPermissionStatus only when it matches; otherwise
|
|
85
85
|
// we fall back to DENIED: a runtime guard at the trust boundary, not an `as`.
|
|
86
86
|
function toPermissionStatus(value) {
|
|
87
|
-
if (value === RESULTS.GRANTED ||
|
|
87
|
+
if (value === RESULTS.GRANTED ||
|
|
88
|
+
value === RESULTS.DENIED ||
|
|
89
|
+
value === RESULTS.NEVER_ASK_AGAIN) {
|
|
88
90
|
return value;
|
|
89
91
|
}
|
|
90
92
|
return RESULTS.DENIED;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IPlatformStatic } from './shared';
|
|
2
|
-
export type { IPlatformOSType, IPlatformSelectSpec, IPlatformStatic } from './shared';
|
|
2
|
+
export type { IPlatformOSType, IPlatformSelectSpec, IPlatformStatic, } from './shared';
|
|
3
3
|
export interface IPlatformConstantsAndroid {
|
|
4
4
|
isTesting: boolean;
|
|
5
5
|
isDisableAnimations?: boolean;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// from the PlatformConstants native module (RN spec NativePlatformConstantsAndroid.js,
|
|
4
4
|
// a different payload from iOS). Metro picks this on an Android host. The module name is
|
|
5
5
|
// the same 'PlatformConstants'; only the shape differs.
|
|
6
|
-
import { createConstantsResolver } from './shared.js';
|
|
6
|
+
import { createConstantsResolver, } from './shared.js';
|
|
7
7
|
// The filename already selected this host: 'android' is a literal, not a probe.
|
|
8
8
|
const OS_ANDROID = 'android';
|
|
9
9
|
// RN reads isTV off the uiMode constant.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type IPlatformStatic } from './shared';
|
|
2
|
-
export type { IPlatformOSType, IPlatformSelectSpec, IPlatformStatic } from './shared';
|
|
2
|
+
export type { IPlatformOSType, IPlatformSelectSpec, IPlatformStatic, } from './shared';
|
|
3
3
|
export interface IPlatformConstantsIOS {
|
|
4
4
|
forceTouchAvailable: boolean;
|
|
5
5
|
interfaceIdiom: string;
|
|
@@ -21,7 +21,9 @@ export function DynamicColorIOS(tuple) {
|
|
|
21
21
|
// True for the opaque objects above: the color seam uses this to route them
|
|
22
22
|
// through the platform processor alongside CSS-string colors.
|
|
23
23
|
export function isOpaqueColorValue(value) {
|
|
24
|
-
return typeof value === 'object' &&
|
|
24
|
+
return (typeof value === 'object' &&
|
|
25
|
+
value !== null &&
|
|
26
|
+
('semantic' in value || 'dynamic' in value));
|
|
25
27
|
}
|
|
26
28
|
// Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++ color
|
|
27
29
|
// parser silently drops strings. The actual conversion (processColor) is RN-platform-specific,
|
package/build/post-commit.d.ts
CHANGED
package/build/post-commit.js
CHANGED
|
@@ -10,6 +10,12 @@ const hooks = new Set();
|
|
|
10
10
|
export function registerPostCommit(hook) {
|
|
11
11
|
hooks.add(hook);
|
|
12
12
|
}
|
|
13
|
+
// The counterpart, needed the moment a hook belongs to something with a lifetime (a screen, a
|
|
14
|
+
// component) rather than to the process. Without it a mounted-and-gone consumer keeps running
|
|
15
|
+
// after every commit forever, holding its whole closure alive.
|
|
16
|
+
export function unregisterPostCommit(hook) {
|
|
17
|
+
hooks.delete(hook);
|
|
18
|
+
}
|
|
13
19
|
export function runPostCommitHooks() {
|
|
14
20
|
for (const hook of hooks)
|
|
15
21
|
hook();
|
|
@@ -15,16 +15,23 @@ const COLOR_STOP_PARTS_REGEX = /\S+\([^)]*\)|\S+/g;
|
|
|
15
15
|
const WHITESPACE_NORMALIZE_REGEX = /\s+/g;
|
|
16
16
|
const LINEAR_GRADIENT_DIRECTION_REGEX = /^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/i;
|
|
17
17
|
const LINEAR_GRADIENT_ANGLE_UNIT_REGEX = /^([+-]?\d*\.?\d+)(deg|grad|rad|turn)$/i;
|
|
18
|
-
const LINEAR_GRADIENT_DEFAULT_DIRECTION = {
|
|
18
|
+
const LINEAR_GRADIENT_DEFAULT_DIRECTION = {
|
|
19
|
+
type: 'angle',
|
|
20
|
+
value: 180,
|
|
21
|
+
};
|
|
19
22
|
const DEFAULT_RADIAL_SHAPE = 'ellipse';
|
|
20
23
|
const DEFAULT_RADIAL_SIZE = 'farthest-corner';
|
|
21
|
-
const DEFAULT_RADIAL_POSITION = {
|
|
24
|
+
const DEFAULT_RADIAL_POSITION = {
|
|
25
|
+
top: '50%',
|
|
26
|
+
left: '50%',
|
|
27
|
+
};
|
|
22
28
|
function isStringOrNumber(value) {
|
|
23
29
|
return typeof value === 'string' || typeof value === 'number';
|
|
24
30
|
}
|
|
25
31
|
// A color-stop position is a plain px number or a percentage string; anything else is invalid.
|
|
26
32
|
function isPositionValue(value) {
|
|
27
|
-
return typeof value === 'number' ||
|
|
33
|
+
return (typeof value === 'number' ||
|
|
34
|
+
(typeof value === 'string' && value.endsWith('%')));
|
|
28
35
|
}
|
|
29
36
|
// A gradient color may already be a platform int (array form) or a CSS string/PlatformColor
|
|
30
37
|
// object needing processColor; mirrors process-box-shadow's processShadowColor.
|
|
@@ -63,7 +70,9 @@ function getAngleInDegrees(angle) {
|
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
72
|
function getDirectionForKeyword(direction) {
|
|
66
|
-
const normalized = direction
|
|
73
|
+
const normalized = direction
|
|
74
|
+
.replace(WHITESPACE_NORMALIZE_REGEX, ' ')
|
|
75
|
+
.toLowerCase();
|
|
67
76
|
switch (normalized) {
|
|
68
77
|
case 'to top':
|
|
69
78
|
return { type: 'angle', value: 0 };
|
|
@@ -106,7 +115,9 @@ function resolveRadialSize(value) {
|
|
|
106
115
|
value === 'farthest-corner') {
|
|
107
116
|
return value;
|
|
108
117
|
}
|
|
109
|
-
if (isRecord(value) &&
|
|
118
|
+
if (isRecord(value) &&
|
|
119
|
+
isStringOrNumber(value.x) &&
|
|
120
|
+
isStringOrNumber(value.y)) {
|
|
110
121
|
return { x: value.x, y: value.y };
|
|
111
122
|
}
|
|
112
123
|
return null;
|
|
@@ -129,7 +140,9 @@ function processColorStopsArray(rawColorStops) {
|
|
|
129
140
|
return null;
|
|
130
141
|
const positions = rawStop.positions;
|
|
131
142
|
// Color transition hint syntax (`red, 20%, blue`): a position with no color of its own.
|
|
132
|
-
if (rawStop.color == null &&
|
|
143
|
+
if (rawStop.color == null &&
|
|
144
|
+
Array.isArray(positions) &&
|
|
145
|
+
positions.length === 1) {
|
|
133
146
|
const position = positions[0];
|
|
134
147
|
if (!isPositionValue(position))
|
|
135
148
|
return null;
|
|
@@ -205,7 +218,13 @@ function processBackgroundImageArray(rawList) {
|
|
|
205
218
|
const position = isRadialPositionValue(rawBgImage.position)
|
|
206
219
|
? rawBgImage.position
|
|
207
220
|
: DEFAULT_RADIAL_POSITION;
|
|
208
|
-
result.push({
|
|
221
|
+
result.push({
|
|
222
|
+
type: 'radial-gradient',
|
|
223
|
+
shape,
|
|
224
|
+
size,
|
|
225
|
+
position,
|
|
226
|
+
colorStops,
|
|
227
|
+
});
|
|
209
228
|
}
|
|
210
229
|
}
|
|
211
230
|
return result;
|
|
@@ -365,11 +384,13 @@ function parseRadialGradientPositionTokens(tokens) {
|
|
|
365
384
|
const token2 = t2.toLowerCase().trim();
|
|
366
385
|
const horizontalPositions = ['left', 'center', 'right'];
|
|
367
386
|
const verticalPositions = ['top', 'center', 'bottom'];
|
|
368
|
-
if (horizontalPositions.includes(token1) &&
|
|
387
|
+
if (horizontalPositions.includes(token1) &&
|
|
388
|
+
verticalPositions.includes(token2)) {
|
|
369
389
|
left = token1 === 'left' ? '0%' : token1 === 'center' ? '50%' : '100%';
|
|
370
390
|
top = token2 === 'top' ? '0%' : token2 === 'center' ? '50%' : '100%';
|
|
371
391
|
}
|
|
372
|
-
else if (verticalPositions.includes(token1) &&
|
|
392
|
+
else if (verticalPositions.includes(token1) &&
|
|
393
|
+
horizontalPositions.includes(token2)) {
|
|
373
394
|
left = token2 === 'left' ? '0%' : token2 === 'center' ? '50%' : '100%';
|
|
374
395
|
top = token1 === 'top' ? '0%' : token1 === 'center' ? '50%' : '100%';
|
|
375
396
|
}
|
package/build/process-filter.js
CHANGED
|
@@ -74,7 +74,9 @@ export function processFilter(filter) {
|
|
|
74
74
|
for (const filterFunction of filter) {
|
|
75
75
|
const [filterName, filterValue] = Object.entries(filterFunction)[0];
|
|
76
76
|
if (filterName === 'dropShadow') {
|
|
77
|
-
const dropShadow = isDropShadowValue(filterValue)
|
|
77
|
+
const dropShadow = isDropShadowValue(filterValue)
|
|
78
|
+
? parseDropShadow(filterValue)
|
|
79
|
+
: null;
|
|
78
80
|
if (dropShadow == null) {
|
|
79
81
|
dlog(`processFilter reject: invalid dropShadow in array`);
|
|
80
82
|
return [];
|
|
@@ -154,7 +156,9 @@ function getFilterAmount(filterName, filterArgs) {
|
|
|
154
156
|
if (unit !== 'deg' && unit !== 'rad') {
|
|
155
157
|
return undefined;
|
|
156
158
|
}
|
|
157
|
-
return unit === 'rad'
|
|
159
|
+
return unit === 'rad'
|
|
160
|
+
? RADIANS_TO_DEGREES * filterArgAsNumber
|
|
161
|
+
: filterArgAsNumber;
|
|
158
162
|
// blur takes any non-negative CSS length that is not a percent; RN only has DIPs.
|
|
159
163
|
case 'blur':
|
|
160
164
|
if ((unit != null && unit !== 'px') || filterArgAsNumber < 0) {
|
|
@@ -169,7 +173,8 @@ function getFilterAmount(filterName, filterArgs) {
|
|
|
169
173
|
case 'opacity':
|
|
170
174
|
case 'saturate':
|
|
171
175
|
case 'sepia':
|
|
172
|
-
if ((unit != null && unit !== '%' && unit !== 'px') ||
|
|
176
|
+
if ((unit != null && unit !== '%' && unit !== 'px') ||
|
|
177
|
+
filterArgAsNumber < 0) {
|
|
173
178
|
return undefined;
|
|
174
179
|
}
|
|
175
180
|
return unit === '%' ? filterArgAsNumber / 100 : filterArgAsNumber;
|
|
@@ -179,7 +184,9 @@ function getFilterAmount(filterName, filterArgs) {
|
|
|
179
184
|
}
|
|
180
185
|
// RN processFilter.js:188-256.
|
|
181
186
|
function parseDropShadow(rawDropShadow) {
|
|
182
|
-
const dropShadow = typeof rawDropShadow === 'string'
|
|
187
|
+
const dropShadow = typeof rawDropShadow === 'string'
|
|
188
|
+
? parseDropShadowString(rawDropShadow)
|
|
189
|
+
: rawDropShadow;
|
|
183
190
|
if (dropShadow == null) {
|
|
184
191
|
return null;
|
|
185
192
|
}
|