@rn-tools/core 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +3 -0
- package/android/build.gradle +47 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/rntoolscore/RNToolsCoreModule.kt +118 -0
- package/android/src/main/java/expo/modules/rntoolscore/SafeAreaUtils.kt +87 -0
- package/expo-module.config.json +17 -0
- package/ios/RNToolsCore.podspec +30 -0
- package/ios/RNToolsCoreModule.swift +110 -0
- package/mocks/react-native.mock.ts +21 -0
- package/mocks/render-node-probe.tsx +50 -0
- package/mocks/setup.ts +6 -0
- package/package.json +37 -0
- package/src/index.ts +4 -0
- package/src/keyboard.ts +43 -0
- package/src/render-tree.test.tsx +483 -0
- package/src/render-tree.tsx +580 -0
- package/src/safe-area.ts +83 -0
- package/src/store.tsx +82 -0
- package/tsconfig.json +10 -0
- package/vitest.config.mts +21 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createStore, useStore } from "./store";
|
|
3
|
+
import type { Store } from "./store";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Each RenderTree creates a store that holds a RenderTreeState object.
|
|
7
|
+
* Nodes register themselves in that store keyed by id.
|
|
8
|
+
* Nodes know their parent (id), type, and active flag.
|
|
9
|
+
*
|
|
10
|
+
* Depth and active are derived on demand:
|
|
11
|
+
* - depth counts how many ancestors share the same type (stack within stack, etc)
|
|
12
|
+
* - active is shared across types and becomes false if any parent is inactive
|
|
13
|
+
*
|
|
14
|
+
* The id is either user-defined or generated (rc:type-1, rc:type-2, ...).
|
|
15
|
+
* The root node has a constant id: "render-tree-root".
|
|
16
|
+
*
|
|
17
|
+
* Tree example (ids omitted):
|
|
18
|
+
* stack-1 (type: stack, depth 1, active true)
|
|
19
|
+
* └── screen-1 (type: screen, depth 1, active true)
|
|
20
|
+
* └── stack-2 (type: stack, depth 2, active true)
|
|
21
|
+
*
|
|
22
|
+
* Note: children are stored as ids.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type RenderTreeType = string;
|
|
26
|
+
|
|
27
|
+
export type RenderTreeOptions = {
|
|
28
|
+
type: RenderTreeType;
|
|
29
|
+
id?: string;
|
|
30
|
+
active?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type RenderNode = {
|
|
34
|
+
id: string;
|
|
35
|
+
type: RenderTreeType;
|
|
36
|
+
parentId: string | null;
|
|
37
|
+
active: boolean;
|
|
38
|
+
children: string[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const RENDER_TREE_GENERATED_ID_PREFIX = "rt:";
|
|
42
|
+
|
|
43
|
+
const nextRenderTreeIdForType = (() => {
|
|
44
|
+
const counters = new Map<RenderTreeType, number>();
|
|
45
|
+
return (type: RenderTreeType) => {
|
|
46
|
+
const next = (counters.get(type) ?? 0) + 1;
|
|
47
|
+
counters.set(type, next);
|
|
48
|
+
return `${RENDER_TREE_GENERATED_ID_PREFIX}${type}-${next}`;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
|
|
52
|
+
export type RenderTreeState = {
|
|
53
|
+
nodes: Map<string, RenderNode>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type RenderTreeDebugNode = {
|
|
57
|
+
id: string;
|
|
58
|
+
type: RenderTreeType;
|
|
59
|
+
parentId: string | null;
|
|
60
|
+
active: boolean;
|
|
61
|
+
depth: number;
|
|
62
|
+
children: RenderTreeDebugNode[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const RENDER_TREE_ROOT_ID = "render-tree-root";
|
|
66
|
+
|
|
67
|
+
function createRootNode(): RenderNode {
|
|
68
|
+
return {
|
|
69
|
+
id: RENDER_TREE_ROOT_ID,
|
|
70
|
+
type: "root",
|
|
71
|
+
parentId: null,
|
|
72
|
+
active: true,
|
|
73
|
+
children: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createRenderTree(nodes?: Map<string, RenderNode>): RenderTreeState {
|
|
78
|
+
const nextNodes = nodes ? new Map(nodes) : new Map();
|
|
79
|
+
if (!nextNodes.has(RENDER_TREE_ROOT_ID)) {
|
|
80
|
+
nextNodes.set(RENDER_TREE_ROOT_ID, createRootNode());
|
|
81
|
+
}
|
|
82
|
+
return { nodes: nextNodes };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getRenderNode(
|
|
86
|
+
chart: RenderTreeState,
|
|
87
|
+
id: string,
|
|
88
|
+
): RenderNode | null {
|
|
89
|
+
return chart.nodes.get(id) ?? null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getRenderNodeParent(
|
|
93
|
+
chart: RenderTreeState,
|
|
94
|
+
id: string,
|
|
95
|
+
): RenderNode | null {
|
|
96
|
+
const node = chart.nodes.get(id);
|
|
97
|
+
if (!node?.parentId) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return chart.nodes.get(node.parentId) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getRenderNodeChildren(
|
|
104
|
+
chart: RenderTreeState,
|
|
105
|
+
id: string,
|
|
106
|
+
): RenderNode[] {
|
|
107
|
+
const node = chart.nodes.get(id);
|
|
108
|
+
if (!node) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return node.children
|
|
112
|
+
.map((childId) => chart.nodes.get(childId))
|
|
113
|
+
.filter((child): child is RenderNode => Boolean(child));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getRenderNodeDepth(
|
|
117
|
+
chart: RenderTreeState,
|
|
118
|
+
id: string,
|
|
119
|
+
type?: RenderTreeType,
|
|
120
|
+
) {
|
|
121
|
+
const node = chart.nodes.get(id);
|
|
122
|
+
if (!node) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
const targetType = type ?? node.type;
|
|
126
|
+
let depth = 0;
|
|
127
|
+
let current: RenderNode | undefined = node;
|
|
128
|
+
while (current) {
|
|
129
|
+
if (current.type === targetType) {
|
|
130
|
+
depth += 1;
|
|
131
|
+
}
|
|
132
|
+
current = current.parentId ? chart.nodes.get(current.parentId) : undefined;
|
|
133
|
+
}
|
|
134
|
+
return depth;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getRenderNodeActive(chart: RenderTreeState, id: string) {
|
|
138
|
+
const node = chart.nodes.get(id);
|
|
139
|
+
if (!node) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
let active = node.active;
|
|
143
|
+
let currentId = node.parentId;
|
|
144
|
+
while (currentId) {
|
|
145
|
+
const current = chart.nodes.get(currentId);
|
|
146
|
+
if (!current) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
active = active && current.active;
|
|
150
|
+
currentId = current.parentId;
|
|
151
|
+
}
|
|
152
|
+
return active;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildRenderTreeDebugTree(
|
|
156
|
+
chart: RenderTreeState,
|
|
157
|
+
): RenderTreeDebugNode | null {
|
|
158
|
+
const root = chart.nodes.get(RENDER_TREE_ROOT_ID);
|
|
159
|
+
if (!root) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const visited = new Set<string>();
|
|
164
|
+
|
|
165
|
+
const buildNode = (node: RenderNode): RenderTreeDebugNode => {
|
|
166
|
+
if (visited.has(node.id)) {
|
|
167
|
+
return {
|
|
168
|
+
id: node.id,
|
|
169
|
+
type: node.type,
|
|
170
|
+
parentId: node.parentId,
|
|
171
|
+
active: getRenderNodeActive(chart, node.id),
|
|
172
|
+
depth: getRenderNodeDepth(chart, node.id),
|
|
173
|
+
children: [],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
visited.add(node.id);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
id: node.id,
|
|
180
|
+
type: node.type,
|
|
181
|
+
parentId: node.parentId,
|
|
182
|
+
active: getRenderNodeActive(chart, node.id),
|
|
183
|
+
depth: getRenderNodeDepth(chart, node.id),
|
|
184
|
+
children: node.children
|
|
185
|
+
.map((childId) => chart.nodes.get(childId))
|
|
186
|
+
.filter((child): child is RenderNode => Boolean(child))
|
|
187
|
+
.map((child) => buildNode(child)),
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return buildNode(root);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function logRenderTreeDebugTree(
|
|
195
|
+
chart: RenderTreeState,
|
|
196
|
+
label = "RenderTreeDebugTree",
|
|
197
|
+
) {
|
|
198
|
+
const tree = buildRenderTreeDebugTree(chart);
|
|
199
|
+
if (!tree) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
console.log(label, tree);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const RenderTreeStoreContext =
|
|
206
|
+
React.createContext<Store<RenderTreeState> | null>(null);
|
|
207
|
+
|
|
208
|
+
export const RenderNodeIdContext = React.createContext<string | null>(null);
|
|
209
|
+
export const RenderNodeTypeContext = React.createContext<RenderTreeType | null>(
|
|
210
|
+
null,
|
|
211
|
+
);
|
|
212
|
+
// TODO: add coverage for node type context/hook wiring.
|
|
213
|
+
|
|
214
|
+
function registerRenderNode(
|
|
215
|
+
store: Store<RenderTreeState>,
|
|
216
|
+
nodeId: string,
|
|
217
|
+
options: RenderTreeOptions,
|
|
218
|
+
parentId: string | null,
|
|
219
|
+
) {
|
|
220
|
+
store.setState((tree) => {
|
|
221
|
+
const existing = tree.nodes.get(nodeId);
|
|
222
|
+
const nextNode: RenderNode = {
|
|
223
|
+
id: nodeId,
|
|
224
|
+
type: options.type,
|
|
225
|
+
parentId,
|
|
226
|
+
active: options.active ?? true,
|
|
227
|
+
children: existing ? existing.children : [],
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const shouldUpdateParent = existing?.parentId !== parentId;
|
|
231
|
+
const isSameNode = existing && areNodesEqual(existing, nextNode);
|
|
232
|
+
if (isSameNode && !shouldUpdateParent) {
|
|
233
|
+
return tree;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const nodes = new Map(tree.nodes);
|
|
237
|
+
const previousParentId = existing?.parentId ?? null;
|
|
238
|
+
|
|
239
|
+
nodes.set(nodeId, nextNode);
|
|
240
|
+
ensureChildrenForParent(nodes, nodeId);
|
|
241
|
+
if (previousParentId && previousParentId !== parentId) {
|
|
242
|
+
removeChildFromParent(nodes, previousParentId, nodeId);
|
|
243
|
+
}
|
|
244
|
+
if (parentId) {
|
|
245
|
+
addChildToParent(nodes, parentId, nodeId);
|
|
246
|
+
}
|
|
247
|
+
return createRenderTree(nodes);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function unregisterRenderNode(store: Store<RenderTreeState>, nodeId: string) {
|
|
252
|
+
store.setState((tree) => {
|
|
253
|
+
if (!tree.nodes.has(nodeId)) {
|
|
254
|
+
return tree;
|
|
255
|
+
}
|
|
256
|
+
const nodes = new Map(tree.nodes);
|
|
257
|
+
const node = nodes.get(nodeId);
|
|
258
|
+
if (!node) {
|
|
259
|
+
return tree;
|
|
260
|
+
}
|
|
261
|
+
const subtreeIds = collectSubtreeIds(nodes, nodeId);
|
|
262
|
+
subtreeIds.forEach((id) => {
|
|
263
|
+
nodes.delete(id);
|
|
264
|
+
});
|
|
265
|
+
if (node.parentId) {
|
|
266
|
+
removeChildFromParent(nodes, node.parentId, nodeId);
|
|
267
|
+
}
|
|
268
|
+
return createRenderTree(nodes);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Root provider for a render tree.
|
|
274
|
+
*
|
|
275
|
+
* Usage:
|
|
276
|
+
* <RenderTree>
|
|
277
|
+
* <Stack />
|
|
278
|
+
* </RenderTree>
|
|
279
|
+
*/
|
|
280
|
+
export type RenderTreeProps = {
|
|
281
|
+
children: React.ReactNode;
|
|
282
|
+
store?: Store<RenderTreeState>;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export type RenderTreeStore = Store<RenderTreeState>;
|
|
286
|
+
|
|
287
|
+
export function createRenderTreeStore(initial?: RenderTreeState): RenderTreeStore {
|
|
288
|
+
return createStore(initial ? createRenderTree(initial.nodes) : createRenderTree());
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export const RenderTree = React.memo(function RenderTree(props: RenderTreeProps) {
|
|
292
|
+
const storeRef = React.useRef(createRenderTreeStore());
|
|
293
|
+
const store = props.store ?? storeRef.current;
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<RenderTreeStoreContext.Provider value={store}>
|
|
297
|
+
<RenderNodeIdContext.Provider value={RENDER_TREE_ROOT_ID}>
|
|
298
|
+
<RenderNodeTypeContext.Provider value="root">
|
|
299
|
+
{props.children}
|
|
300
|
+
</RenderNodeTypeContext.Provider>
|
|
301
|
+
</RenderNodeIdContext.Provider>
|
|
302
|
+
</RenderTreeStoreContext.Provider>
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Registers a node in the render tree.
|
|
308
|
+
*
|
|
309
|
+
* Usage:
|
|
310
|
+
* <RenderNode type="stack">
|
|
311
|
+
* ...children...
|
|
312
|
+
* </RenderNode>
|
|
313
|
+
*
|
|
314
|
+
* `active` defaults to true. Passing `active={false}` disables the subtree.
|
|
315
|
+
* `id` is optional; if omitted, a stable id is generated.
|
|
316
|
+
*/
|
|
317
|
+
export function RenderTreeNode(
|
|
318
|
+
props: RenderTreeOptions & {
|
|
319
|
+
children: React.ReactNode;
|
|
320
|
+
},
|
|
321
|
+
) {
|
|
322
|
+
const store = React.useContext(RenderTreeStoreContext);
|
|
323
|
+
const parentId = React.useContext(RenderNodeIdContext);
|
|
324
|
+
const nodeIdRef = React.useRef(
|
|
325
|
+
props.id ?? nextRenderTreeIdForType(props.type),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (!store) {
|
|
329
|
+
throw new Error("RenderTree is missing from the component tree.");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
React.useLayoutEffect(() => {
|
|
333
|
+
registerRenderNode(
|
|
334
|
+
store,
|
|
335
|
+
nodeIdRef.current,
|
|
336
|
+
{ type: props.type, active: props.active },
|
|
337
|
+
parentId,
|
|
338
|
+
);
|
|
339
|
+
}, [store, props.type, props.active, parentId]);
|
|
340
|
+
|
|
341
|
+
React.useEffect(
|
|
342
|
+
() => () => unregisterRenderNode(store, nodeIdRef.current),
|
|
343
|
+
[store],
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<RenderNodeIdContext.Provider value={nodeIdRef.current}>
|
|
348
|
+
<RenderNodeTypeContext.Provider value={props.type}>
|
|
349
|
+
{props.children}
|
|
350
|
+
</RenderNodeTypeContext.Provider>
|
|
351
|
+
</RenderNodeIdContext.Provider>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function useRenderNode(): RenderNode | null {
|
|
356
|
+
const store = React.useContext(RenderTreeStoreContext);
|
|
357
|
+
const nodeId = React.useContext(RenderNodeIdContext);
|
|
358
|
+
if (!store) {
|
|
359
|
+
throw new Error("RenderTree is missing from the component tree.");
|
|
360
|
+
}
|
|
361
|
+
if (!nodeId) {
|
|
362
|
+
throw new Error("RenderNode is missing from the component tree.");
|
|
363
|
+
}
|
|
364
|
+
const node = useStore(
|
|
365
|
+
store,
|
|
366
|
+
(state) => getRenderNode(state, nodeId),
|
|
367
|
+
areRenderNodesEqual,
|
|
368
|
+
);
|
|
369
|
+
return node;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function useRenderNodeId(): string {
|
|
373
|
+
const store = React.useContext(RenderTreeStoreContext);
|
|
374
|
+
const nodeId = React.useContext(RenderNodeIdContext);
|
|
375
|
+
if (!store) {
|
|
376
|
+
throw new Error("RenderTree is missing from the component tree.");
|
|
377
|
+
}
|
|
378
|
+
if (!nodeId) {
|
|
379
|
+
throw new Error("RenderNode is missing from the component tree.");
|
|
380
|
+
}
|
|
381
|
+
return nodeId;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function useRenderNodeIdOfType(type: RenderTreeType): string {
|
|
385
|
+
const store = React.useContext(RenderTreeStoreContext);
|
|
386
|
+
const nodeId = React.useContext(RenderNodeIdContext);
|
|
387
|
+
if (!store) {
|
|
388
|
+
throw new Error("RenderTree is missing from the component tree.");
|
|
389
|
+
}
|
|
390
|
+
if (!nodeId) {
|
|
391
|
+
throw new Error("RenderNode is missing from the component tree.");
|
|
392
|
+
}
|
|
393
|
+
const match = useStore(store, (state) => {
|
|
394
|
+
let current = getRenderNode(state, nodeId);
|
|
395
|
+
while (current) {
|
|
396
|
+
if (current.type === type) {
|
|
397
|
+
return current.id;
|
|
398
|
+
}
|
|
399
|
+
if (!current.parentId) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
current = getRenderNode(state, current.parentId);
|
|
403
|
+
}
|
|
404
|
+
return null;
|
|
405
|
+
});
|
|
406
|
+
if (!match) {
|
|
407
|
+
throw new Error(`RenderNode of type "${type}" is missing.`);
|
|
408
|
+
}
|
|
409
|
+
return match;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function useRenderNodeType(): RenderTreeType {
|
|
413
|
+
const store = React.useContext(RenderTreeStoreContext);
|
|
414
|
+
const type = React.useContext(RenderNodeTypeContext);
|
|
415
|
+
if (!store) {
|
|
416
|
+
throw new Error("RenderTree is missing from the component tree.");
|
|
417
|
+
}
|
|
418
|
+
if (!type) {
|
|
419
|
+
throw new Error("RenderNode is missing from the component tree.");
|
|
420
|
+
}
|
|
421
|
+
return type;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Select a slice of the current node's render tree.
|
|
426
|
+
*
|
|
427
|
+
* Usage:
|
|
428
|
+
* const depth = useRenderTreeSelector((chart, id) =>
|
|
429
|
+
* getRenderNodeDepth(chart, id),
|
|
430
|
+
* );
|
|
431
|
+
*/
|
|
432
|
+
export function useRenderTreeSelector<S>(
|
|
433
|
+
selector: (chart: RenderTreeState, id: string) => S,
|
|
434
|
+
isEqual?: (left: S, right: S) => boolean,
|
|
435
|
+
): S | null {
|
|
436
|
+
const store = React.useContext(RenderTreeStoreContext);
|
|
437
|
+
const nodeId = React.useContext(RenderNodeIdContext);
|
|
438
|
+
if (!store) {
|
|
439
|
+
throw new Error("RenderTree is missing from the component tree.");
|
|
440
|
+
}
|
|
441
|
+
if (!nodeId) {
|
|
442
|
+
throw new Error("RenderNode is missing from the component tree.");
|
|
443
|
+
}
|
|
444
|
+
return useStore(
|
|
445
|
+
store,
|
|
446
|
+
(state) => {
|
|
447
|
+
if (!state.nodes.has(nodeId)) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
return selector(state, nodeId);
|
|
451
|
+
},
|
|
452
|
+
isEqual as typeof isEqual,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function areRenderNodesEqual(
|
|
457
|
+
left: RenderNode | null,
|
|
458
|
+
right: RenderNode | null,
|
|
459
|
+
) {
|
|
460
|
+
if (left === right) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
if (!left || !right) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
return areNodesEqual(left, right);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function areNodesEqual(left: RenderNode, right: RenderNode) {
|
|
470
|
+
return (
|
|
471
|
+
left.id === right.id &&
|
|
472
|
+
left.type === right.type &&
|
|
473
|
+
left.parentId === right.parentId &&
|
|
474
|
+
left.active === right.active &&
|
|
475
|
+
areStringArraysEqual(left.children, right.children)
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function areStringArraysEqual(left: string[], right: string[]) {
|
|
480
|
+
if (left === right) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
if (left.length !== right.length) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
487
|
+
if (left[index] !== right[index]) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function addChildToParent(
|
|
495
|
+
nodes: Map<string, RenderNode>,
|
|
496
|
+
parentId: string,
|
|
497
|
+
childId: string,
|
|
498
|
+
) {
|
|
499
|
+
const parent = nodes.get(parentId);
|
|
500
|
+
if (!parent) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (parent.children.includes(childId)) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
nodes.set(parentId, {
|
|
507
|
+
...parent,
|
|
508
|
+
children: [...parent.children, childId],
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function removeChildFromParent(
|
|
513
|
+
nodes: Map<string, RenderNode>,
|
|
514
|
+
parentId: string,
|
|
515
|
+
childId: string,
|
|
516
|
+
) {
|
|
517
|
+
const parent = nodes.get(parentId);
|
|
518
|
+
if (!parent) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (!parent.children.includes(childId)) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
nodes.set(parentId, {
|
|
525
|
+
...parent,
|
|
526
|
+
children: parent.children.filter((id) => id !== childId),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function ensureChildrenForParent(
|
|
531
|
+
nodes: Map<string, RenderNode>,
|
|
532
|
+
parentId: string,
|
|
533
|
+
) {
|
|
534
|
+
const parent = nodes.get(parentId);
|
|
535
|
+
if (!parent) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const existing = new Set(parent.children);
|
|
539
|
+
const nextChildren = parent.children.slice();
|
|
540
|
+
nodes.forEach((node) => {
|
|
541
|
+
if (node.parentId === parentId && !existing.has(node.id)) {
|
|
542
|
+
nextChildren.push(node.id);
|
|
543
|
+
existing.add(node.id);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
if (!areStringArraysEqual(parent.children, nextChildren)) {
|
|
547
|
+
nodes.set(parentId, { ...parent, children: nextChildren });
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function collectSubtreeIds(nodes: Map<string, RenderNode>, rootId: string) {
|
|
552
|
+
return collectSubtreeIdsFromRoots(nodes, [rootId]);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function collectSubtreeIdsFromRoots(
|
|
556
|
+
nodes: Map<string, RenderNode>,
|
|
557
|
+
rootIds: string[],
|
|
558
|
+
) {
|
|
559
|
+
const visited = new Set<string>();
|
|
560
|
+
const stack = [...rootIds];
|
|
561
|
+
|
|
562
|
+
while (stack.length > 0) {
|
|
563
|
+
const currentId = stack.pop();
|
|
564
|
+
if (!currentId || visited.has(currentId)) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
visited.add(currentId);
|
|
568
|
+
const node = nodes.get(currentId);
|
|
569
|
+
if (!node) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
node.children.forEach((childId) => {
|
|
573
|
+
if (!visited.has(childId)) {
|
|
574
|
+
stack.push(childId);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return Array.from(visited);
|
|
580
|
+
}
|
package/src/safe-area.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { requireNativeModule } from "expo-modules-core";
|
|
2
|
+
import { createStore, useStore } from "./store";
|
|
3
|
+
|
|
4
|
+
export type EdgeInsets = {
|
|
5
|
+
top: number;
|
|
6
|
+
right: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
left: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SafeAreaState = {
|
|
12
|
+
insets: EdgeInsets;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type SafeAreaInsetsChangeEvent = {
|
|
16
|
+
insets?: EdgeInsets;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type NativeCoreModule = {
|
|
20
|
+
addListener: (
|
|
21
|
+
eventName: "onSafeAreaInsetsChange",
|
|
22
|
+
listener: (event: SafeAreaInsetsChangeEvent) => void,
|
|
23
|
+
) => { remove: () => void };
|
|
24
|
+
getSafeAreaInsets?: () => EdgeInsets;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const nativeCoreModule = requireNativeModule<NativeCoreModule>("RNToolsCore");
|
|
28
|
+
|
|
29
|
+
const fallbackInsets: EdgeInsets = {
|
|
30
|
+
top: 0,
|
|
31
|
+
right: 0,
|
|
32
|
+
bottom: 0,
|
|
33
|
+
left: 0,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function getSafeAreaInsets(): EdgeInsets {
|
|
37
|
+
try {
|
|
38
|
+
return nativeCoreModule?.getSafeAreaInsets?.() ?? fallbackInsets;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return fallbackInsets;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function areInsetsEqual(left: EdgeInsets, right: EdgeInsets) {
|
|
45
|
+
return (
|
|
46
|
+
left.top === right.top &&
|
|
47
|
+
left.right === right.right &&
|
|
48
|
+
left.bottom === right.bottom &&
|
|
49
|
+
left.left === right.left
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const safeAreaStore = createStore<SafeAreaState>({
|
|
54
|
+
insets: getSafeAreaInsets(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
nativeCoreModule.addListener(
|
|
58
|
+
"onSafeAreaInsetsChange",
|
|
59
|
+
(event: SafeAreaInsetsChangeEvent) => {
|
|
60
|
+
const nextInsets = event?.insets ?? getSafeAreaInsets();
|
|
61
|
+
safeAreaStore.setState((state) => {
|
|
62
|
+
if (areInsetsEqual(state.insets, nextInsets)) {
|
|
63
|
+
return state;
|
|
64
|
+
}
|
|
65
|
+
return { ...state, insets: nextInsets };
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const initialInsets = getSafeAreaInsets();
|
|
71
|
+
|
|
72
|
+
safeAreaStore.setState((state) => {
|
|
73
|
+
if (areInsetsEqual(state.insets, initialInsets)) {
|
|
74
|
+
return state;
|
|
75
|
+
}
|
|
76
|
+
return { ...state, insets: initialInsets };
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const insetsSelector = (state: SafeAreaState) => state.insets;
|
|
80
|
+
|
|
81
|
+
export const useSafeAreaInsets = (): EdgeInsets => {
|
|
82
|
+
return useStore(safeAreaStore, insetsSelector);
|
|
83
|
+
};
|