@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/safe-area.ts
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import type { ElementNode, RootNode } from './nodes/node.js';
|
|
2
|
+
import {
|
|
3
|
+
getRootWindowMetrics,
|
|
4
|
+
getWindowViewportForRoot,
|
|
5
|
+
} from '@exact/core/window-state';
|
|
6
|
+
import type { Frame } from '@exact/core/agent/types';
|
|
7
|
+
import type {
|
|
8
|
+
KeyboardState,
|
|
9
|
+
SafeAreaInsets,
|
|
10
|
+
SafeAreaRegionInsets,
|
|
11
|
+
SafeAreaRegionName,
|
|
12
|
+
} from '@exact/core/window-types';
|
|
13
|
+
import type {
|
|
14
|
+
CanonicalStyle,
|
|
15
|
+
ResolvedSafeAreaConfig,
|
|
16
|
+
ResolvedSafeAreaState,
|
|
17
|
+
SafeAreaEdge,
|
|
18
|
+
SafeAreaEdgeMode,
|
|
19
|
+
SafeAreaEdgeModes,
|
|
20
|
+
SafeAreaInsetProp,
|
|
21
|
+
SafeAreaPropagationState,
|
|
22
|
+
SafeAreaRegionSet,
|
|
23
|
+
SafeAreaStrategy,
|
|
24
|
+
} from './types.js';
|
|
25
|
+
import { NodeKind } from './nodes/node.js';
|
|
26
|
+
|
|
27
|
+
const SAFE_AREA_EDGES: readonly SafeAreaEdge[] = ['top', 'right', 'bottom', 'left'] as const;
|
|
28
|
+
const SAFE_AREA_REGIONS: readonly SafeAreaRegionName[] = [
|
|
29
|
+
'container',
|
|
30
|
+
'keyboard',
|
|
31
|
+
'displayCutout',
|
|
32
|
+
'gestures',
|
|
33
|
+
] as const;
|
|
34
|
+
const PADDING_KEYS: Record<SafeAreaEdge, 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft'> = {
|
|
35
|
+
top: 'paddingTop',
|
|
36
|
+
right: 'paddingRight',
|
|
37
|
+
bottom: 'paddingBottom',
|
|
38
|
+
left: 'paddingLeft',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DEFAULT_EDGE_MODES: Readonly<SafeAreaEdgeModes> = Object.freeze({
|
|
42
|
+
top: 'additive',
|
|
43
|
+
right: 'additive',
|
|
44
|
+
bottom: 'additive',
|
|
45
|
+
left: 'additive',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function cloneInsets(insets: SafeAreaInsets): SafeAreaInsets {
|
|
49
|
+
return { ...insets };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function zeroInsets(): SafeAreaInsets {
|
|
53
|
+
return {
|
|
54
|
+
top: 0,
|
|
55
|
+
right: 0,
|
|
56
|
+
bottom: 0,
|
|
57
|
+
left: 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function zeroRegions(): SafeAreaRegionInsets {
|
|
62
|
+
return {
|
|
63
|
+
container: zeroInsets(),
|
|
64
|
+
displayCutout: zeroInsets(),
|
|
65
|
+
gestures: zeroInsets(),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cloneRegions(regions: SafeAreaRegionInsets): SafeAreaRegionInsets {
|
|
70
|
+
return {
|
|
71
|
+
container: cloneInsets(regions.container),
|
|
72
|
+
displayCutout: cloneInsets(regions.displayCutout),
|
|
73
|
+
gestures: cloneInsets(regions.gestures),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cloneKeyboardState(state: KeyboardState): KeyboardState {
|
|
78
|
+
return {
|
|
79
|
+
...state,
|
|
80
|
+
occlusion: { ...state.occlusion },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasInset(insets: SafeAreaInsets): boolean {
|
|
85
|
+
return (
|
|
86
|
+
insets.top > 0 ||
|
|
87
|
+
insets.right > 0 ||
|
|
88
|
+
insets.bottom > 0 ||
|
|
89
|
+
insets.left > 0
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function maxInsets(left: SafeAreaInsets, right: SafeAreaInsets): SafeAreaInsets {
|
|
94
|
+
return {
|
|
95
|
+
top: Math.max(left.top, right.top),
|
|
96
|
+
right: Math.max(left.right, right.right),
|
|
97
|
+
bottom: Math.max(left.bottom, right.bottom),
|
|
98
|
+
left: Math.max(left.left, right.left),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addInsets(left: SafeAreaInsets, right: SafeAreaInsets): SafeAreaInsets {
|
|
103
|
+
return {
|
|
104
|
+
top: left.top + right.top,
|
|
105
|
+
right: left.right + right.right,
|
|
106
|
+
bottom: left.bottom + right.bottom,
|
|
107
|
+
left: left.left + right.left,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function subtractInsets(left: SafeAreaInsets, right: SafeAreaInsets): SafeAreaInsets {
|
|
112
|
+
return {
|
|
113
|
+
top: Math.max(0, left.top - right.top),
|
|
114
|
+
right: Math.max(0, left.right - right.right),
|
|
115
|
+
bottom: Math.max(0, left.bottom - right.bottom),
|
|
116
|
+
left: Math.max(0, left.left - right.left),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function readNumericPadding(
|
|
121
|
+
style: CanonicalStyle,
|
|
122
|
+
edge: SafeAreaEdge,
|
|
123
|
+
viewport: { width: number; height: number },
|
|
124
|
+
): number {
|
|
125
|
+
const source = style[PADDING_KEYS[edge]];
|
|
126
|
+
if (source == null) {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (source.type === 'points') {
|
|
131
|
+
return source.value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (source.type === 'percent') {
|
|
135
|
+
const base = edge === 'left' || edge === 'right'
|
|
136
|
+
? viewport.width
|
|
137
|
+
: viewport.height;
|
|
138
|
+
return base * (source.value / 100);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function setPadding(
|
|
145
|
+
style: CanonicalStyle,
|
|
146
|
+
edge: SafeAreaEdge,
|
|
147
|
+
value: number,
|
|
148
|
+
): void {
|
|
149
|
+
style[PADDING_KEYS[edge]] = { type: 'points', value };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function defaultRegionsForNode(
|
|
153
|
+
tagType: ElementNode['tagType'],
|
|
154
|
+
parent: ElementNode | RootNode | null | undefined,
|
|
155
|
+
): SafeAreaRegionName[] {
|
|
156
|
+
// Direct children of the synthetic root behave as the layout root from the
|
|
157
|
+
// RFC’s point of view. Regular views therefore default to container safety.
|
|
158
|
+
if (tagType === 'scroll') {
|
|
159
|
+
return ['container', 'keyboard'];
|
|
160
|
+
}
|
|
161
|
+
if (parent?.kind === NodeKind.Root) {
|
|
162
|
+
return ['container'];
|
|
163
|
+
}
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeRegionSet(
|
|
168
|
+
regions: SafeAreaRegionSet | undefined,
|
|
169
|
+
fallback: SafeAreaRegionName[],
|
|
170
|
+
): SafeAreaRegionName[] {
|
|
171
|
+
if (regions == null) {
|
|
172
|
+
return fallback.slice();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (regions === 'all') {
|
|
176
|
+
return SAFE_AREA_REGIONS.slice();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (regions === 'none') {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (regions === 'container' || regions === 'keyboard') {
|
|
184
|
+
return [regions];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!Array.isArray(regions)) {
|
|
188
|
+
return fallback.slice();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const next: SafeAreaRegionName[] = [];
|
|
192
|
+
for (const entry of regions) {
|
|
193
|
+
if (isSafeAreaRegionName(entry)) {
|
|
194
|
+
if (!next.includes(entry)) {
|
|
195
|
+
next.push(entry);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return next;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeEdgeModes(
|
|
203
|
+
raw: Record<string, unknown>,
|
|
204
|
+
): SafeAreaEdgeModes {
|
|
205
|
+
const next: SafeAreaEdgeModes = { ...DEFAULT_EDGE_MODES };
|
|
206
|
+
for (const edge of SAFE_AREA_EDGES) {
|
|
207
|
+
const value = raw[edge];
|
|
208
|
+
if (value === 'additive' || value === 'maximum' || value === 'off') {
|
|
209
|
+
next[edge] = value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return next;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveSafeAreaConfig(
|
|
216
|
+
tagType: ElementNode['tagType'],
|
|
217
|
+
parent: ElementNode | RootNode | null | undefined,
|
|
218
|
+
raw: unknown,
|
|
219
|
+
): ResolvedSafeAreaConfig {
|
|
220
|
+
if (raw === 'none') {
|
|
221
|
+
return {
|
|
222
|
+
explicit: true,
|
|
223
|
+
regions: [],
|
|
224
|
+
edges: { ...DEFAULT_EDGE_MODES },
|
|
225
|
+
strategy: tagType === 'scroll' ? 'contentInset' : 'framePadding',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const fallbackRegions = defaultRegionsForNode(tagType, parent);
|
|
230
|
+
if (typeof raw !== 'object' || raw == null) {
|
|
231
|
+
return {
|
|
232
|
+
explicit: false,
|
|
233
|
+
regions: fallbackRegions,
|
|
234
|
+
edges: { ...DEFAULT_EDGE_MODES },
|
|
235
|
+
strategy: tagType === 'scroll' ? 'contentInset' : 'framePadding',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const record = raw as Record<string, unknown>;
|
|
240
|
+
return {
|
|
241
|
+
explicit: true,
|
|
242
|
+
regions: normalizeRegionSet(record.regions as SafeAreaRegionSet | undefined, fallbackRegions),
|
|
243
|
+
edges: normalizeEdgeModes(record),
|
|
244
|
+
strategy: tagType === 'scroll' ? 'contentInset' : 'framePadding',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function readDeclaredSafeAreaInset(
|
|
249
|
+
raw: unknown,
|
|
250
|
+
): SafeAreaInsetProp | undefined {
|
|
251
|
+
if (typeof raw !== 'object' || raw == null) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const record = raw as Record<string, unknown>;
|
|
256
|
+
const edge = record.edge;
|
|
257
|
+
const size = record.size;
|
|
258
|
+
if (
|
|
259
|
+
isSafeAreaEdge(edge) &&
|
|
260
|
+
typeof size === 'number' &&
|
|
261
|
+
Number.isFinite(size) &&
|
|
262
|
+
size > 0
|
|
263
|
+
) {
|
|
264
|
+
return {
|
|
265
|
+
edge,
|
|
266
|
+
size,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isSafeAreaEdge(value: unknown): value is SafeAreaEdge {
|
|
274
|
+
return (SAFE_AREA_EDGES as readonly unknown[]).includes(value);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isSafeAreaRegionName(value: unknown): value is SafeAreaRegionName {
|
|
278
|
+
return (SAFE_AREA_REGIONS as readonly unknown[]).includes(value);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function accumulateSiblingExpansion(
|
|
282
|
+
parent: ElementNode | RootNode | null | undefined,
|
|
283
|
+
currentNode: ElementNode,
|
|
284
|
+
): SafeAreaInsets {
|
|
285
|
+
if (!parent || (parent.kind !== NodeKind.Root && parent.kind !== NodeKind.Element)) {
|
|
286
|
+
return zeroInsets();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const expansion = zeroInsets();
|
|
290
|
+
for (const child of parent.children) {
|
|
291
|
+
if (child.kind !== NodeKind.Element || child.id === currentNode.id) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const declaredInset = readDeclaredSafeAreaInset(child.originalProps.safeAreaInset);
|
|
296
|
+
if (!declaredInset) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
expansion[declaredInset.edge] += declaredInset.size;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return expansion;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function readRenderedFrame(
|
|
307
|
+
node: ElementNode,
|
|
308
|
+
rootId: number,
|
|
309
|
+
): Frame | null {
|
|
310
|
+
const viewport = getWindowViewportForRoot(rootId);
|
|
311
|
+
const exactBridge = (
|
|
312
|
+
globalThis as {
|
|
313
|
+
exact?: {
|
|
314
|
+
getAbsoluteLayout?: (viewId: number) => Frame | null;
|
|
315
|
+
getLayout?: (viewId: number) => Frame | null;
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
).exact;
|
|
319
|
+
|
|
320
|
+
const frame = exactBridge?.getAbsoluteLayout?.(node.id) ?? exactBridge?.getLayout?.(node.id) ?? null;
|
|
321
|
+
if (frame) {
|
|
322
|
+
return frame;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Before the first layout pass there may be no host frame yet. For the first
|
|
326
|
+
// child under the synthetic root we can still use the root viewport as a
|
|
327
|
+
// useful approximation, which is enough for docked-keyboard heuristics.
|
|
328
|
+
if (node.parent?.kind === NodeKind.Root) {
|
|
329
|
+
return {
|
|
330
|
+
x: 0,
|
|
331
|
+
y: 0,
|
|
332
|
+
width: viewport.width,
|
|
333
|
+
height: viewport.height,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function computeKeyboardOverlap(
|
|
341
|
+
frame: Frame | null,
|
|
342
|
+
keyboard: KeyboardState,
|
|
343
|
+
viewport: { width: number; height: number },
|
|
344
|
+
): SafeAreaInsets {
|
|
345
|
+
if (!keyboard.visible || keyboard.occlusion.width <= 0 || keyboard.occlusion.height <= 0) {
|
|
346
|
+
return zeroInsets();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const occlusion = keyboard.occlusion;
|
|
350
|
+
|
|
351
|
+
// Docked keyboards can fall back to a pure bottom inset even when the node
|
|
352
|
+
// has not been laid out yet. Floating/split keyboards require an actual frame
|
|
353
|
+
// because globalizing them would recreate the very bug this RFC is avoiding.
|
|
354
|
+
if (!frame) {
|
|
355
|
+
if (keyboard.mode === 'docked') {
|
|
356
|
+
return {
|
|
357
|
+
top: 0,
|
|
358
|
+
right: 0,
|
|
359
|
+
bottom: Math.max(0, viewport.height - occlusion.y),
|
|
360
|
+
left: 0,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return zeroInsets();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const horizontallyOverlaps =
|
|
367
|
+
frame.x < occlusion.x + occlusion.width &&
|
|
368
|
+
frame.x + frame.width > occlusion.x;
|
|
369
|
+
const verticallyOverlaps =
|
|
370
|
+
frame.y < occlusion.y + occlusion.height &&
|
|
371
|
+
frame.y + frame.height > occlusion.y;
|
|
372
|
+
|
|
373
|
+
if (!horizontallyOverlaps || !verticallyOverlaps) {
|
|
374
|
+
return zeroInsets();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
top: 0,
|
|
379
|
+
right: 0,
|
|
380
|
+
bottom: Math.max(0, frame.y + frame.height - occlusion.y),
|
|
381
|
+
left: 0,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function consumeStaticRegions(
|
|
386
|
+
regions: SafeAreaRegionInsets,
|
|
387
|
+
appliedInsets: SafeAreaInsets,
|
|
388
|
+
activeRegions: readonly SafeAreaRegionName[],
|
|
389
|
+
): SafeAreaRegionInsets {
|
|
390
|
+
// Keyboard consumption is geometry-driven and therefore implicit: when a
|
|
391
|
+
// parent shifts itself away from the keyboard, descendants naturally see less
|
|
392
|
+
// or no overlap because their frames move. Static regions are different. They
|
|
393
|
+
// must only be marked "consumed" when this node explicitly opted into that
|
|
394
|
+
// static region; a keyboard-only node must not accidentally zero out the
|
|
395
|
+
// container safe area for its descendants.
|
|
396
|
+
const consumedStaticInsets = activeRegions.some((region) => region === 'container')
|
|
397
|
+
? appliedInsets
|
|
398
|
+
: zeroInsets();
|
|
399
|
+
const consumedDisplayCutoutInsets = activeRegions.some((region) => region === 'displayCutout')
|
|
400
|
+
? appliedInsets
|
|
401
|
+
: zeroInsets();
|
|
402
|
+
const consumedGestureInsets = activeRegions.some((region) => region === 'gestures')
|
|
403
|
+
? appliedInsets
|
|
404
|
+
: zeroInsets();
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
container: subtractInsets(regions.container, consumedStaticInsets),
|
|
408
|
+
displayCutout: subtractInsets(regions.displayCutout, consumedDisplayCutoutInsets),
|
|
409
|
+
gestures: subtractInsets(regions.gestures, consumedGestureInsets),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function deriveAvailableInsets(
|
|
414
|
+
regions: SafeAreaRegionInsets,
|
|
415
|
+
keyboardOverlap: SafeAreaInsets,
|
|
416
|
+
activeRegions: SafeAreaRegionName[],
|
|
417
|
+
): SafeAreaInsets {
|
|
418
|
+
let available = zeroInsets();
|
|
419
|
+
for (const region of activeRegions) {
|
|
420
|
+
switch (region) {
|
|
421
|
+
case 'container':
|
|
422
|
+
available = maxInsets(available, regions.container);
|
|
423
|
+
break;
|
|
424
|
+
case 'displayCutout':
|
|
425
|
+
available = maxInsets(available, regions.displayCutout);
|
|
426
|
+
break;
|
|
427
|
+
case 'gestures':
|
|
428
|
+
available = maxInsets(available, regions.gestures);
|
|
429
|
+
break;
|
|
430
|
+
case 'keyboard':
|
|
431
|
+
available = maxInsets(available, keyboardOverlap);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return available;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function applySafeAreaPadding(
|
|
439
|
+
baseStyle: CanonicalStyle,
|
|
440
|
+
safeAreaInsets: SafeAreaInsets,
|
|
441
|
+
edges: SafeAreaEdgeModes,
|
|
442
|
+
viewport: { width: number; height: number },
|
|
443
|
+
): CanonicalStyle {
|
|
444
|
+
if (!hasInset(safeAreaInsets)) {
|
|
445
|
+
return baseStyle;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const nextStyle: CanonicalStyle = { ...baseStyle };
|
|
449
|
+
for (const edge of SAFE_AREA_EDGES) {
|
|
450
|
+
const safeAreaValue = safeAreaInsets[edge];
|
|
451
|
+
if (safeAreaValue <= 0 || edges[edge] === 'off') {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const basePadding = readNumericPadding(baseStyle, edge, viewport);
|
|
456
|
+
const resolved =
|
|
457
|
+
edges[edge] === 'maximum'
|
|
458
|
+
? Math.max(basePadding, safeAreaValue)
|
|
459
|
+
: basePadding + safeAreaValue;
|
|
460
|
+
setPadding(nextStyle, edge, resolved);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return nextStyle;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function createRootSafeAreaPropagation(
|
|
467
|
+
rootId: number,
|
|
468
|
+
): SafeAreaPropagationState {
|
|
469
|
+
const metrics = getRootWindowMetrics(rootId);
|
|
470
|
+
return {
|
|
471
|
+
rootId,
|
|
472
|
+
regions: cloneRegions(metrics.regions),
|
|
473
|
+
keyboard: cloneKeyboardState(metrics.keyboard),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function resolveSafeAreaForElement(
|
|
478
|
+
node: ElementNode,
|
|
479
|
+
parent: ElementNode | RootNode | null | undefined,
|
|
480
|
+
baseStyle: CanonicalStyle,
|
|
481
|
+
inherited: SafeAreaPropagationState,
|
|
482
|
+
): {
|
|
483
|
+
style: CanonicalStyle;
|
|
484
|
+
safeAreaState: ResolvedSafeAreaState | null;
|
|
485
|
+
propagatedSafeArea: SafeAreaPropagationState;
|
|
486
|
+
} {
|
|
487
|
+
const viewport = getWindowViewportForRoot(inherited.rootId);
|
|
488
|
+
const config = resolveSafeAreaConfig(node.tagType, parent, node.originalProps.safeArea);
|
|
489
|
+
const siblingExpansion = accumulateSiblingExpansion(parent, node);
|
|
490
|
+
const sourceRegions = cloneRegions(inherited.regions);
|
|
491
|
+
sourceRegions.container = addInsets(sourceRegions.container, siblingExpansion);
|
|
492
|
+
|
|
493
|
+
const frame = readRenderedFrame(node, inherited.rootId);
|
|
494
|
+
const keyboardOverlap =
|
|
495
|
+
config.regions.includes('keyboard')
|
|
496
|
+
? computeKeyboardOverlap(frame, inherited.keyboard, viewport)
|
|
497
|
+
: zeroInsets();
|
|
498
|
+
const availableInsets = deriveAvailableInsets(sourceRegions, keyboardOverlap, config.regions);
|
|
499
|
+
|
|
500
|
+
// `maximum` still counts as consumption even when author padding is already
|
|
501
|
+
// large enough. Descendants should not re-apply the same safe area again.
|
|
502
|
+
const appliedInsets = cloneInsets(availableInsets);
|
|
503
|
+
for (const edge of SAFE_AREA_EDGES) {
|
|
504
|
+
if (config.edges[edge] === 'off') {
|
|
505
|
+
appliedInsets[edge] = 0;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const style = applySafeAreaPadding(baseStyle, appliedInsets, config.edges, viewport);
|
|
510
|
+
const remainingRegions = consumeStaticRegions(
|
|
511
|
+
sourceRegions,
|
|
512
|
+
appliedInsets,
|
|
513
|
+
config.regions,
|
|
514
|
+
);
|
|
515
|
+
const propagatedSafeArea: SafeAreaPropagationState = {
|
|
516
|
+
rootId: inherited.rootId,
|
|
517
|
+
regions: remainingRegions,
|
|
518
|
+
keyboard: cloneKeyboardState(inherited.keyboard),
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const shouldExposeState =
|
|
522
|
+
config.explicit ||
|
|
523
|
+
config.regions.length > 0 ||
|
|
524
|
+
hasInset(siblingExpansion);
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
style,
|
|
528
|
+
safeAreaState: shouldExposeState
|
|
529
|
+
? {
|
|
530
|
+
config,
|
|
531
|
+
strategy: config.strategy,
|
|
532
|
+
sourceRegions,
|
|
533
|
+
siblingExpansion,
|
|
534
|
+
availableInsets,
|
|
535
|
+
keyboardOverlap,
|
|
536
|
+
appliedInsets,
|
|
537
|
+
remainingRegions,
|
|
538
|
+
keyboardState: cloneKeyboardState(inherited.keyboard),
|
|
539
|
+
}
|
|
540
|
+
: null,
|
|
541
|
+
propagatedSafeArea,
|
|
542
|
+
};
|
|
543
|
+
}
|