@webview-native-ui/engine 0.0.4 → 0.0.6
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 +1 -0
- package/dist/dom/visibility.js +1 -0
- package/dist/projection/scope.js +1 -1
- package/dist/projection/styles.d.ts +2 -2
- package/dist/projection/styles.js +20 -9
- package/dist/projection/web.js +259 -185
- package/dist/types.d.ts +4 -0
- package/docs/capacitor-plugin.md +4 -0
- package/docs/cordova-plugin.md +4 -0
- package/docs/projection-containers.md +195 -0
- package/docs/projection.md +30 -16
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +35 -22
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +20 -12
- package/ios/Sources/WebViewNativeUIEngine/ScrollBindings.swift +1 -0
- package/ios/Sources/WebViewNativeUIEngine/ScrollTracker.swift +1 -1
- package/package.json +2 -2
package/dist/projection/web.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { projectionURL } from '../native/resource-url.js';
|
|
2
2
|
import { bounded, boundedListener } from '../native/bridge.js';
|
|
3
3
|
import { bindProjectionEvents } from './events.js';
|
|
4
|
-
import {
|
|
5
|
-
import { snapshotStyles } from './styles.js';
|
|
4
|
+
import { snapshotStyles, snapshotShadowStyles } from './styles.js';
|
|
6
5
|
const layout = (candidates, positionIndependent = false) => JSON.stringify(candidates.map(({ control: c }) => [
|
|
7
6
|
c.id,
|
|
8
7
|
c.kind,
|
|
@@ -14,41 +13,6 @@ const layout = (candidates, positionIndependent = false) => JSON.stringify(candi
|
|
|
14
13
|
c.dark,
|
|
15
14
|
c.rtl,
|
|
16
15
|
]));
|
|
17
|
-
// Native frames are viewport pixels; absolute children use the target's local
|
|
18
|
-
// padding box. Keep the conversion independent of theme dimensions.
|
|
19
|
-
const coordinateSpace = (target, win, style = win.getComputedStyle(target)) => {
|
|
20
|
-
const rect = target.getBoundingClientRect();
|
|
21
|
-
// offsetWidth/Height round fractional CSS sizes, which would introduce drift.
|
|
22
|
-
const borderBox = (size, sides) => {
|
|
23
|
-
const value = parseFloat(size);
|
|
24
|
-
return style.display !== 'inline' && Number.isFinite(value)
|
|
25
|
-
? value +
|
|
26
|
-
(style.boxSizing === 'border-box' ? 0 : sides.reduce((sum, side) => sum + (parseFloat(style.getPropertyValue(side)) || 0), 0))
|
|
27
|
-
: NaN;
|
|
28
|
-
};
|
|
29
|
-
const width = borderBox(style.width, ['padding-left', 'padding-right', 'border-left-width', 'border-right-width']);
|
|
30
|
-
const height = borderBox(style.height, ['padding-top', 'padding-bottom', 'border-top-width', 'border-bottom-width']);
|
|
31
|
-
// Inline dimensions may be auto (or an ignored specified width). Native element
|
|
32
|
-
// projection excludes transformed ancestors; use CSS zoom, not rounded offsets.
|
|
33
|
-
let zoom = 1;
|
|
34
|
-
if (!(width > 0 && height > 0)) {
|
|
35
|
-
for (const element of [target, ...projectionAncestors(target)]) {
|
|
36
|
-
const value = (element === target ? style : win.getComputedStyle(element)).getPropertyValue('zoom');
|
|
37
|
-
zoom *= (parseFloat(value) || 1) / (value.endsWith('%') ? 100 : 1);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const scaleX = width > 0 ? rect.width / width : zoom;
|
|
41
|
-
const scaleY = height > 0 ? rect.height / height : zoom;
|
|
42
|
-
return {
|
|
43
|
-
x: rect.x,
|
|
44
|
-
y: rect.y,
|
|
45
|
-
scaleX: scaleX || 1,
|
|
46
|
-
scaleY: scaleY || 1,
|
|
47
|
-
left: (parseFloat(style.borderLeftWidth) || 0) - target.scrollLeft,
|
|
48
|
-
top: (parseFloat(style.borderTopWidth) || 0) - target.scrollTop,
|
|
49
|
-
position: style.position,
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
16
|
/** Images are prepared after accepted native updates, never on the overlay's critical path. */
|
|
53
17
|
const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
54
18
|
const captureDelayMs = callbacks.captureDelayMs ?? 50;
|
|
@@ -61,6 +25,16 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
61
25
|
const imageIdentity = Array.from(win.crypto.getRandomValues(new Uint32Array(4)), (value) => value.toString(16)).join('-');
|
|
62
26
|
const validSource = (source) => source.isConnected && source.ownerDocument === doc;
|
|
63
27
|
const validTarget = (target) => !!target?.isConnected && target.ownerDocument === doc;
|
|
28
|
+
// Keep current, preparing and pending-release images leased; reuse idle DOM slots.
|
|
29
|
+
const imagePool = new Map();
|
|
30
|
+
const leasedImages = new Set();
|
|
31
|
+
const retireImage = (image) => {
|
|
32
|
+
leasedImages.delete(image);
|
|
33
|
+
image.style.setProperty('display', 'none', 'important');
|
|
34
|
+
image.removeAttribute('data-native-ui-projection-image');
|
|
35
|
+
image.removeAttribute('src');
|
|
36
|
+
pruneIdleImages();
|
|
37
|
+
};
|
|
64
38
|
const call = transport.send.bind(transport);
|
|
65
39
|
const imageMarker = 'data-native-ui-snapshot';
|
|
66
40
|
const sourcesChangedEvent = `${scope.namespace}:sources-change`;
|
|
@@ -70,7 +44,11 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
70
44
|
const signature = (candidates) => JSON.stringify(candidates.map((candidate) => {
|
|
71
45
|
if (!scopeIDs.has(candidate.scope))
|
|
72
46
|
scopeIDs.set(candidate.scope, ++scopeSerial);
|
|
73
|
-
|
|
47
|
+
const origin = callbacks.transient && !callbacks.opaqueUnderlay ? candidate.scope.element.getBoundingClientRect() : undefined;
|
|
48
|
+
const relative = origin
|
|
49
|
+
? { ...candidate, control: { ...candidate.control, x: candidate.control.x - origin.x, y: candidate.control.y - origin.y } }
|
|
50
|
+
: candidate;
|
|
51
|
+
return [scopeIDs.get(candidate.scope), layout([relative], !!callbacks.opaqueUnderlay)];
|
|
74
52
|
}));
|
|
75
53
|
const sheets = new Map();
|
|
76
54
|
const rootFor = (element) => {
|
|
@@ -78,17 +56,48 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
78
56
|
return root.nodeType === 11 && 'host' in root ? root : doc;
|
|
79
57
|
};
|
|
80
58
|
const sheetFor = (root) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
59
|
+
if (sheets.has(root))
|
|
60
|
+
return;
|
|
61
|
+
const style = doc.createElement('style');
|
|
62
|
+
style.setAttribute('data-native-ui-snapshot-base', '');
|
|
63
|
+
style.textContent = snapshotStyles + (root.nodeType === 11 ? snapshotShadowStyles : '');
|
|
64
|
+
(root.nodeType === 11 ? root : root.head).append(style);
|
|
65
|
+
sheets.set(root, style);
|
|
66
|
+
};
|
|
67
|
+
// Each shadow tree is a separate style scope. Keep these rules alongside the
|
|
68
|
+
// prepared image; only the marker changes during the handoff.
|
|
69
|
+
const finishVisibilityTransitions = (target) => {
|
|
70
|
+
// A visibility transition can keep the old content painted over the image.
|
|
71
|
+
// Do not stop layout, transform or opacity transitions owned by the renderer.
|
|
72
|
+
const animations = [
|
|
73
|
+
...target.getAnimations({ subtree: true }),
|
|
74
|
+
...[...(shadowHosts.get(target) ?? [])].flatMap((host) => host.shadowRoot?.getAnimations() ?? []),
|
|
75
|
+
];
|
|
76
|
+
for (const animation of animations)
|
|
77
|
+
if ('transitionProperty' in animation && animation.transitionProperty === 'visibility')
|
|
78
|
+
animation.cancel();
|
|
79
|
+
};
|
|
80
|
+
const shadowHosts = new Map();
|
|
81
|
+
const syncShadowContent = (target) => {
|
|
82
|
+
const hosts = shadowHosts.get(target) ?? new Set();
|
|
83
|
+
shadowHosts.set(target, hosts);
|
|
84
|
+
const hidden = target.hasAttribute('data-native-ui-snapshot-source') || target.hasAttribute('data-native-ui-snapshot-prepared');
|
|
85
|
+
const visit = (element) => {
|
|
86
|
+
if (element.hasAttribute('data-native-ui-projection-node'))
|
|
87
|
+
return;
|
|
88
|
+
if (element.shadowRoot) {
|
|
89
|
+
hosts.add(element);
|
|
90
|
+
sheetFor(element.shadowRoot);
|
|
91
|
+
for (const child of element.shadowRoot.children)
|
|
92
|
+
visit(child);
|
|
93
|
+
}
|
|
94
|
+
for (const child of element.children)
|
|
95
|
+
visit(child);
|
|
96
|
+
};
|
|
97
|
+
visit(target);
|
|
98
|
+
for (const host of hosts)
|
|
99
|
+
host.toggleAttribute('data-native-ui-snapshot-content', hidden);
|
|
100
|
+
finishVisibilityTransitions(target);
|
|
92
101
|
};
|
|
93
102
|
const viewport = () => `${win.innerWidth}:${win.innerHeight}:${win.devicePixelRatio}`;
|
|
94
103
|
let handoffPending = false;
|
|
@@ -109,12 +118,46 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
109
118
|
let stopped = false;
|
|
110
119
|
let flight = false;
|
|
111
120
|
let latest;
|
|
121
|
+
const pruneIdleImages = () => {
|
|
122
|
+
const targets = new Set(latest?.candidates.map((candidate) => candidate.scope.element));
|
|
123
|
+
for (const [target, pool] of imagePool) {
|
|
124
|
+
const reusable = !stopped && target.isConnected && targets.has(target);
|
|
125
|
+
if (reusable && (active || ending || handoffPending))
|
|
126
|
+
continue;
|
|
127
|
+
const limit = reusable ? 2 * Math.max(1, cached?.scopes.filter((scope) => scope.element === target).length ?? 0) : 0;
|
|
128
|
+
let spare = Math.max(0, limit - pool.filter((image) => leasedImages.has(image)).length);
|
|
129
|
+
const retained = pool.filter((image) => {
|
|
130
|
+
if (leasedImages.has(image) || spare-- > 0)
|
|
131
|
+
return true;
|
|
132
|
+
image.remove();
|
|
133
|
+
return false;
|
|
134
|
+
});
|
|
135
|
+
if (retained.length)
|
|
136
|
+
imagePool.set(target, retained);
|
|
137
|
+
else
|
|
138
|
+
imagePool.delete(target);
|
|
139
|
+
}
|
|
140
|
+
const liveRoots = new Set([
|
|
141
|
+
...(latest?.candidates.map((candidate) => candidate.scope.element.getRootNode()) ?? []),
|
|
142
|
+
...(active?.scopes.map((scope) => scope.element.getRootNode()) ?? []),
|
|
143
|
+
...(cached?.scopes.map((scope) => scope.element.getRootNode()) ?? []),
|
|
144
|
+
...[...leasedImages].map((image) => image.getRootNode()),
|
|
145
|
+
...[...shadowHosts.values()].flatMap((hosts) => [...hosts].map((host) => host.shadowRoot)),
|
|
146
|
+
]);
|
|
147
|
+
for (const [root, pair] of sheets)
|
|
148
|
+
if (root.nodeType === 11 && !liveRoots.has(root)) {
|
|
149
|
+
pair.remove();
|
|
150
|
+
sheets.delete(root);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
112
153
|
const pendingCleanup = new Set();
|
|
113
154
|
const cleanup = (action, token, afterPaint) => {
|
|
114
155
|
const run = () => {
|
|
115
156
|
if (!pendingCleanup.delete(run))
|
|
116
157
|
return;
|
|
117
158
|
void call({ action, token }).catch(() => { });
|
|
159
|
+
if (action === 'restored')
|
|
160
|
+
pruneIdleImages();
|
|
118
161
|
};
|
|
119
162
|
pendingCleanup.add(run);
|
|
120
163
|
if (afterPaint && !stopped)
|
|
@@ -131,7 +174,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
131
174
|
if (!item || item === cached || item === active)
|
|
132
175
|
return;
|
|
133
176
|
item.images.forEach((image) => {
|
|
134
|
-
image
|
|
177
|
+
retireImage(image);
|
|
135
178
|
});
|
|
136
179
|
cleanup('release', item.data.token, afterPaint);
|
|
137
180
|
};
|
|
@@ -207,7 +250,19 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
207
250
|
if (result.parts.some((part) => new Set(part.ids.map((id) => selected.find((candidate) => candidate.control.id === id).scope)).size !== 1))
|
|
208
251
|
return;
|
|
209
252
|
await bounded(Promise.all(result.parts.map(async (part) => {
|
|
210
|
-
const
|
|
253
|
+
const target = selected.find((candidate) => part.ids.includes(candidate.control.id)).scope.element;
|
|
254
|
+
let pool = imagePool.get(target);
|
|
255
|
+
if (!pool) {
|
|
256
|
+
pool = [];
|
|
257
|
+
imagePool.set(target, pool);
|
|
258
|
+
}
|
|
259
|
+
let image = pool.find((item) => !leasedImages.has(item));
|
|
260
|
+
if (!image) {
|
|
261
|
+
image = doc.createElement('img');
|
|
262
|
+
image.setAttribute('data-native-ui-projection-node', '');
|
|
263
|
+
pool.push(image);
|
|
264
|
+
}
|
|
265
|
+
leasedImages.add(image);
|
|
211
266
|
images.push(image);
|
|
212
267
|
image.src = projectionURL(part.url);
|
|
213
268
|
await image.decode();
|
|
@@ -224,7 +279,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
224
279
|
selected.some((candidate) => !validTarget(candidate.scope.element) || ![candidate.element, ...(candidate.sources ?? [])].every(validSource)))
|
|
225
280
|
return;
|
|
226
281
|
const previous = cached;
|
|
227
|
-
|
|
282
|
+
const prepared = {
|
|
228
283
|
data: result,
|
|
229
284
|
images,
|
|
230
285
|
sources: Array.from(new Set(selected.flatMap((item) => [item.element, ...(item.sources ?? [])]))),
|
|
@@ -236,12 +291,32 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
236
291
|
revision,
|
|
237
292
|
layout: signature(selected),
|
|
238
293
|
};
|
|
294
|
+
try {
|
|
295
|
+
if (!render(prepared))
|
|
296
|
+
throw new Error('Projection destination is unavailable');
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
// Do not publish a token whose DOM could not be installed. Restore the
|
|
300
|
+
// last completed presentation before retiring the failed preparation.
|
|
301
|
+
detach();
|
|
302
|
+
try {
|
|
303
|
+
render();
|
|
304
|
+
cached?.sources.forEach((source) => {
|
|
305
|
+
preparedOwners.add(source);
|
|
306
|
+
preparedSources.add(source);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
abandon();
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
cached = prepared;
|
|
239
315
|
clearPrepared();
|
|
240
316
|
cached.sources.forEach((source) => {
|
|
241
317
|
preparedOwners.add(source);
|
|
242
318
|
preparedSources.add(source);
|
|
243
319
|
});
|
|
244
|
-
render();
|
|
245
320
|
if (callbacks.opaqueUnderlay)
|
|
246
321
|
void call({ action: 'prepared', token: result.token }).catch(() => { });
|
|
247
322
|
win.dispatchEvent(new Event(sourcesChangedEvent));
|
|
@@ -255,7 +330,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
255
330
|
finally {
|
|
256
331
|
if (!adopted)
|
|
257
332
|
images.forEach((image) => {
|
|
258
|
-
image
|
|
333
|
+
retireImage(image);
|
|
259
334
|
});
|
|
260
335
|
if (result?.token && !adopted)
|
|
261
336
|
void call({ action: 'release', token: result.token }).catch(() => { });
|
|
@@ -291,6 +366,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
291
366
|
if (latest?.revision !== revision)
|
|
292
367
|
generation++;
|
|
293
368
|
latest = { revision, candidates };
|
|
369
|
+
pruneIdleImages();
|
|
294
370
|
if (cached?.revision !== revision || preparedVisualRevision !== visualRevision)
|
|
295
371
|
schedule();
|
|
296
372
|
};
|
|
@@ -304,7 +380,6 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
304
380
|
invalidate();
|
|
305
381
|
if (active) {
|
|
306
382
|
updateOffsets();
|
|
307
|
-
followBorderAnimation();
|
|
308
383
|
}
|
|
309
384
|
visualRevision++;
|
|
310
385
|
generation++;
|
|
@@ -321,17 +396,21 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
321
396
|
}));
|
|
322
397
|
const preparedTargets = new Set();
|
|
323
398
|
const clearTarget = (target) => {
|
|
399
|
+
shadowHosts.get(target)?.forEach((host) => host.removeAttribute('data-native-ui-snapshot-content'));
|
|
324
400
|
if (preparedTargets.delete(target))
|
|
325
401
|
target.removeAttribute('data-native-ui-snapshot-prepared');
|
|
326
402
|
target.removeAttribute('data-native-ui-snapshot-visible');
|
|
327
403
|
target.removeAttribute(imageMarker);
|
|
404
|
+
target.removeAttribute('data-native-ui-snapshot-positioned');
|
|
328
405
|
target.removeAttribute('data-native-ui-snapshot-source');
|
|
406
|
+
finishVisibilityTransitions(target);
|
|
407
|
+
shadowHosts.delete(target);
|
|
329
408
|
};
|
|
330
409
|
const detach = () => {
|
|
331
410
|
presentation++;
|
|
332
411
|
win.cancelAnimationFrame(paintFrame);
|
|
333
|
-
|
|
334
|
-
|
|
412
|
+
for (const element of offsets.keys())
|
|
413
|
+
element.style.setProperty('opacity', '0', 'important');
|
|
335
414
|
offsets.clear();
|
|
336
415
|
paintFrame = 0;
|
|
337
416
|
handoffPending = false;
|
|
@@ -343,11 +422,12 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
343
422
|
});
|
|
344
423
|
rendered.forEach(clearTarget);
|
|
345
424
|
rendered.clear();
|
|
346
|
-
sheets.forEach((
|
|
347
|
-
|
|
348
|
-
|
|
425
|
+
sheets.forEach((style, root) => {
|
|
426
|
+
if (stopped || (root.nodeType === 11 && !root.host.isConnected)) {
|
|
427
|
+
style.remove();
|
|
428
|
+
sheets.delete(root);
|
|
429
|
+
}
|
|
349
430
|
});
|
|
350
|
-
sheets.clear();
|
|
351
431
|
};
|
|
352
432
|
const abandon = (expected = active, afterPaint = ending) => {
|
|
353
433
|
if (active !== expected)
|
|
@@ -371,7 +451,16 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
371
451
|
return;
|
|
372
452
|
}
|
|
373
453
|
const image = active;
|
|
374
|
-
|
|
454
|
+
presentation++;
|
|
455
|
+
win.cancelAnimationFrame(paintFrame);
|
|
456
|
+
paintFrame = 0;
|
|
457
|
+
handoffPending = presented = false;
|
|
458
|
+
image.sources.forEach((source) => {
|
|
459
|
+
owners.delete(source);
|
|
460
|
+
visibleOwners.delete(source);
|
|
461
|
+
});
|
|
462
|
+
rendered.forEach((target) => target.removeAttribute('data-native-ui-snapshot-source'));
|
|
463
|
+
showImages(image, !!callbacks.opaqueUnderlay);
|
|
375
464
|
active = undefined;
|
|
376
465
|
ending = false;
|
|
377
466
|
win.clearTimeout(restoreTimeout);
|
|
@@ -385,7 +474,6 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
385
474
|
preparedOwners.add(source);
|
|
386
475
|
preparedSources.add(source);
|
|
387
476
|
});
|
|
388
|
-
render();
|
|
389
477
|
callbacks.wake();
|
|
390
478
|
};
|
|
391
479
|
const retireRestoration = () => {
|
|
@@ -395,51 +483,60 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
395
483
|
};
|
|
396
484
|
const targetsFor = (image) => image.scopes.map((scope) => (validTarget(scope.element) ? scope.element : undefined));
|
|
397
485
|
const rendered = new Set();
|
|
486
|
+
const showImages = (image, visible) => {
|
|
487
|
+
for (const target of rendered) {
|
|
488
|
+
target.toggleAttribute('data-native-ui-snapshot-visible', visible);
|
|
489
|
+
syncShadowContent(target);
|
|
490
|
+
}
|
|
491
|
+
image.images.forEach((element) => element.style.setProperty('opacity', visible ? '1' : '0', 'important'));
|
|
492
|
+
};
|
|
398
493
|
const preparedKeys = new Map();
|
|
399
494
|
const offsets = new Map();
|
|
400
|
-
let offsetFrame = 0;
|
|
401
495
|
const updateOffsets = () => {
|
|
402
|
-
|
|
403
|
-
|
|
496
|
+
const writes = [];
|
|
497
|
+
// Read all geometry before writing positions; never reset a painted image to zero to measure it.
|
|
498
|
+
for (const [element, placement] of offsets) {
|
|
499
|
+
const { target, frame } = placement;
|
|
500
|
+
if (!target.isConnected) {
|
|
501
|
+
element.remove();
|
|
404
502
|
continue;
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
?.getKeyframes()
|
|
422
|
-
.some((frame) => ['borderLeftWidth', 'borderTopWidth', 'borderWidth'].some((property) => property in frame))));
|
|
423
|
-
if (movingBorder)
|
|
424
|
-
offsetFrame = win.requestAnimationFrame(() => {
|
|
425
|
-
offsetFrame = 0;
|
|
426
|
-
updateOffsets();
|
|
427
|
-
followBorderAnimation();
|
|
503
|
+
}
|
|
504
|
+
const parent = target.shadowRoot ?? target;
|
|
505
|
+
if (element.parentNode !== parent)
|
|
506
|
+
parent.insertBefore(element, null);
|
|
507
|
+
const box = target.getBoundingClientRect();
|
|
508
|
+
const rect = element.getBoundingClientRect();
|
|
509
|
+
const width = parseFloat(element.style.width) || frame.width;
|
|
510
|
+
const height = parseFloat(element.style.height) || frame.height;
|
|
511
|
+
const sx = rect.width / width || 1, sy = rect.height / height || 1;
|
|
512
|
+
const initial = placement.scale ?? (placement.scale = { x: sx, y: sy });
|
|
513
|
+
writes.push({
|
|
514
|
+
element,
|
|
515
|
+
left: (parseFloat(element.style.left) || 0) + (box.x + (frame.x * sx) / initial.x - rect.x) / sx,
|
|
516
|
+
top: (parseFloat(element.style.top) || 0) + (box.y + (frame.y * sy) / initial.y - rect.y) / sy,
|
|
517
|
+
width: frame.width / initial.x,
|
|
518
|
+
height: frame.height / initial.y,
|
|
428
519
|
});
|
|
520
|
+
}
|
|
521
|
+
for (const { element, ...values } of writes)
|
|
522
|
+
for (const [property, value] of Object.entries(values)) {
|
|
523
|
+
const next = `${value}px`;
|
|
524
|
+
if (element.style.getPropertyValue(property) !== next)
|
|
525
|
+
element.style.setProperty(property, next, 'important');
|
|
526
|
+
}
|
|
429
527
|
};
|
|
430
|
-
const render = () => {
|
|
431
|
-
const image = active ?? cached;
|
|
528
|
+
const render = (image = active ?? cached) => {
|
|
432
529
|
if (!image || ending)
|
|
433
|
-
return;
|
|
530
|
+
return false;
|
|
434
531
|
if (!image.sources.every(validSource) || targetsFor(image).some((target) => !target)) {
|
|
435
532
|
invalidate();
|
|
436
|
-
return;
|
|
533
|
+
return false;
|
|
437
534
|
}
|
|
438
535
|
const targets = targetsFor(image);
|
|
439
536
|
if (active) {
|
|
440
537
|
if (targets.some((target) => !target?.isConnected || !sheets.has(rootFor(target)))) {
|
|
441
538
|
abandon(active, false);
|
|
442
|
-
return;
|
|
539
|
+
return false;
|
|
443
540
|
}
|
|
444
541
|
const next = new Set();
|
|
445
542
|
for (const [index, source] of image.targets.entries()) {
|
|
@@ -449,7 +546,6 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
449
546
|
continue;
|
|
450
547
|
target.setAttribute(imageMarker, key);
|
|
451
548
|
target.setAttribute('data-native-ui-snapshot-source', '');
|
|
452
|
-
target.toggleAttribute('data-native-ui-snapshot-visible', presented);
|
|
453
549
|
next.add(target);
|
|
454
550
|
}
|
|
455
551
|
for (const target of rendered)
|
|
@@ -457,84 +553,70 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
457
553
|
clearTarget(target);
|
|
458
554
|
rendered.clear();
|
|
459
555
|
next.forEach((target) => rendered.add(target));
|
|
460
|
-
|
|
556
|
+
showImages(image, presented);
|
|
557
|
+
return true;
|
|
461
558
|
}
|
|
559
|
+
imagePool.forEach((pool) => pool.forEach((element) => {
|
|
560
|
+
if (!image.images.includes(element)) {
|
|
561
|
+
element.style.setProperty('display', 'none', 'important');
|
|
562
|
+
element.removeAttribute('data-native-ui-projection-image');
|
|
563
|
+
}
|
|
564
|
+
}));
|
|
462
565
|
preparedKeys.clear();
|
|
463
566
|
offsets.clear();
|
|
464
567
|
rendered.forEach(clearTarget);
|
|
465
568
|
rendered.clear();
|
|
466
|
-
// Reuse the image stylesheet. Presentation states belong to the runtime's
|
|
467
|
-
// permanent CSS, which survives cache replacement and image retirement.
|
|
468
|
-
const groups = new Map();
|
|
469
569
|
for (const target of new Set(targets)) {
|
|
470
570
|
if (!target)
|
|
471
571
|
continue;
|
|
472
572
|
rendered.add(target);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
const { rules, positions } = group;
|
|
480
|
-
// A decoded clone stays concealed even if this cache is invalidated before
|
|
481
|
-
// the projection adapter publishes its next Native/Web ownership state.
|
|
482
|
-
if (target !== image.targets[targets.indexOf(target)] && !target.hasAttribute('data-native-ui-snapshot-prepared')) {
|
|
573
|
+
sheetFor(rootFor(target));
|
|
574
|
+
const indices = targets.flatMap((owner, index) => (owner === target ? [index] : []));
|
|
575
|
+
const key = `${imageIdentity}-${indices[0]}`;
|
|
576
|
+
target.setAttribute(imageMarker, key);
|
|
577
|
+
if (target !== image.targets[indices[0]]) {
|
|
483
578
|
target.setAttribute('data-native-ui-snapshot-prepared', '');
|
|
484
579
|
preparedTargets.add(target);
|
|
485
580
|
}
|
|
486
|
-
target.toggleAttribute('data-native-ui-snapshot-visible', !!
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
{
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
581
|
+
target.toggleAttribute('data-native-ui-snapshot-visible', !!callbacks.opaqueUnderlay);
|
|
582
|
+
syncShadowContent(target);
|
|
583
|
+
const box = target.getBoundingClientRect();
|
|
584
|
+
target.toggleAttribute('data-native-ui-snapshot-positioned', win.getComputedStyle(target).position === 'static');
|
|
585
|
+
const parent = target.shadowRoot ?? target;
|
|
586
|
+
for (const index of [...indices].reverse()) {
|
|
587
|
+
const element = image.images[index];
|
|
588
|
+
const part = image.data.parts[index];
|
|
589
|
+
const frame = image.frames[index] ?? {
|
|
590
|
+
x: callbacks.opaqueUnderlay ? 0 : part.x - box.x,
|
|
591
|
+
y: callbacks.opaqueUnderlay ? 0 : part.y - box.y,
|
|
592
|
+
width: part.width,
|
|
593
|
+
height: part.height,
|
|
594
|
+
};
|
|
595
|
+
image.frames[index] = frame;
|
|
596
|
+
preparedKeys.set(image.targets[index], key);
|
|
597
|
+
element.inert = true;
|
|
598
|
+
element.setAttribute('aria-hidden', 'true');
|
|
599
|
+
element.setAttribute('data-native-ui-projection-image', key);
|
|
600
|
+
element.style.cssText =
|
|
601
|
+
'all:initial!important;position:absolute!important;display:block!important;visibility:visible!important;pointer-events:none!important;image-rendering:pixelated!important;will-change:opacity!important;opacity:0!important;';
|
|
602
|
+
element.style.setProperty('width', `${frame.width}px`, 'important');
|
|
603
|
+
element.style.setProperty('height', `${frame.height}px`, 'important');
|
|
604
|
+
element.style.setProperty('left', '0px', 'important');
|
|
605
|
+
element.style.setProperty('top', '0px', 'important');
|
|
606
|
+
element.style.setProperty('opacity', callbacks.opaqueUnderlay ? '1' : '0', 'important');
|
|
607
|
+
if (element.parentNode !== parent)
|
|
608
|
+
parent.insertBefore(element, null);
|
|
609
|
+
offsets.set(element, { target, frame });
|
|
504
610
|
}
|
|
505
|
-
const frames = indices.map((index) => image.frames[index]);
|
|
506
|
-
const x = Math.min(...frames.map((frame) => frame.x));
|
|
507
|
-
const y = Math.min(...frames.map((frame) => frame.y));
|
|
508
|
-
const right = Math.max(...frames.map((frame) => frame.x + frame.width));
|
|
509
|
-
const bottom = Math.max(...frames.map((frame) => frame.y + frame.height));
|
|
510
|
-
const left = (parseFloat(style.borderLeftWidth) || 0) - target.scrollLeft;
|
|
511
|
-
const top = (parseFloat(style.borderTopWidth) || 0) - target.scrollTop;
|
|
512
|
-
const key = `${imageIdentity}-${indices[0]}`;
|
|
513
|
-
indices.forEach((index) => preparedKeys.set(image.targets[index], key));
|
|
514
|
-
target.setAttribute(imageMarker, key);
|
|
515
|
-
rules.push(`[${imageMarker}="${key}"][data-native-ui-snapshot-visible] { ${style.position === 'static' ? 'position:relative!important;' : ''} }`);
|
|
516
|
-
const backgrounds = indices.map((index) => {
|
|
517
|
-
const frame = image.frames[index];
|
|
518
|
-
return `url(${JSON.stringify(image.images[index].src)}) ${frame.x - x}px ${frame.y - y}px / ${frame.width}px ${frame.height}px no-repeat`;
|
|
519
|
-
});
|
|
520
|
-
const declaration = doc.createElement('span').style;
|
|
521
|
-
declaration.cssText = `left:${x - left}px;top:${y - top}px;width:${right - x}px;height:${bottom - y}px;background:${backgrounds.join(',')};`;
|
|
522
|
-
for (const property of Array.from(declaration))
|
|
523
|
-
declaration.setProperty(property, declaration.getPropertyValue(property), 'important');
|
|
524
|
-
// Outrank every manager's shared reset, even when another manager starts later.
|
|
525
|
-
rules.push(`[${imageMarker}="${key}"][${imageMarker}][${imageMarker}]::before { ${declaration.cssText} }`);
|
|
526
|
-
positions.push({ target, x, y });
|
|
527
611
|
}
|
|
528
612
|
image.parents = image.scopes.map((scope) => scope.element.parentNode);
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
positions.forEach(({ target, x, y }, index) => offsets.set(target, {
|
|
532
|
-
x,
|
|
533
|
-
y,
|
|
534
|
-
style: sheet.sheet.cssRules[index * 2 + 1].style,
|
|
535
|
-
}));
|
|
536
|
-
}
|
|
613
|
+
updateOffsets();
|
|
614
|
+
return true;
|
|
537
615
|
};
|
|
616
|
+
const currentParts = (image) => image.images.map((element, index) => {
|
|
617
|
+
const rect = element.getBoundingClientRect();
|
|
618
|
+
return { ...image.data.parts[index], x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
619
|
+
});
|
|
538
620
|
const begin = () => {
|
|
539
621
|
retireRestoration();
|
|
540
622
|
if (stopped || destination === 'none' || active)
|
|
@@ -565,10 +647,8 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
565
647
|
image.sources.forEach((source) => {
|
|
566
648
|
owners.add(source);
|
|
567
649
|
});
|
|
568
|
-
rendered.forEach((target) =>
|
|
569
|
-
|
|
570
|
-
target.toggleAttribute('data-native-ui-snapshot-visible', presented);
|
|
571
|
-
});
|
|
650
|
+
rendered.forEach((target) => target.setAttribute('data-native-ui-snapshot-source', ''));
|
|
651
|
+
showImages(image, presented);
|
|
572
652
|
// Reconciliation may continue after one frame even if the optional bridge reply
|
|
573
653
|
// is delayed. The ordered native transport processes cover before that update.
|
|
574
654
|
paintFrame = win.requestAnimationFrame(() => {
|
|
@@ -578,7 +658,11 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
578
658
|
handoffPending = false;
|
|
579
659
|
callbacks.wake();
|
|
580
660
|
});
|
|
581
|
-
void bounded(call({
|
|
661
|
+
void bounded(call({
|
|
662
|
+
action: 'cover',
|
|
663
|
+
token: image.data.token,
|
|
664
|
+
...(callbacks.transient ? { transient: true, viewportWidth: win.innerWidth, parts: currentParts(image) } : {}),
|
|
665
|
+
}))
|
|
582
666
|
.then((result) => {
|
|
583
667
|
if (!isCurrent())
|
|
584
668
|
return;
|
|
@@ -594,7 +678,6 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
594
678
|
image.sources.forEach((source) => visibleOwners.add(source));
|
|
595
679
|
render();
|
|
596
680
|
updateOffsets();
|
|
597
|
-
followBorderAnimation();
|
|
598
681
|
}
|
|
599
682
|
win.cancelAnimationFrame(paintFrame);
|
|
600
683
|
paintFrame = win.requestAnimationFrame(() => {
|
|
@@ -625,6 +708,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
625
708
|
}
|
|
626
709
|
if (callbacks.blocked())
|
|
627
710
|
return;
|
|
711
|
+
updateOffsets();
|
|
628
712
|
if (callbacks.transient) {
|
|
629
713
|
const image = active;
|
|
630
714
|
const targets = targetsFor(image);
|
|
@@ -632,23 +716,14 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
632
716
|
abandon(image, false);
|
|
633
717
|
return;
|
|
634
718
|
}
|
|
635
|
-
const parts =
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
height: frame.height * space.scaleY,
|
|
644
|
-
};
|
|
645
|
-
});
|
|
646
|
-
if (parts.some((part, index) => ['x', 'y', 'width', 'height'].some((key) => part[key] !== image.data.parts[index][key]))) {
|
|
647
|
-
if (callbacks.opaqueUnderlay)
|
|
648
|
-
image.data = { ...image.data, parts };
|
|
649
|
-
else
|
|
650
|
-
cached = undefined;
|
|
651
|
-
}
|
|
719
|
+
const parts = currentParts(image);
|
|
720
|
+
// Translation changes placement, not pixels. Keep the pre-interaction
|
|
721
|
+
// image until its replacement is ready; resizing still invalidates it.
|
|
722
|
+
if (callbacks.opaqueUnderlay ||
|
|
723
|
+
parts.every((part, index) => part.width === image.data.parts[index].width && part.height === image.data.parts[index].height))
|
|
724
|
+
image.data = { ...image.data, parts };
|
|
725
|
+
else
|
|
726
|
+
cached = undefined;
|
|
652
727
|
// Both requests use the same ordered bridge. Reprojection need not await this optional copy.
|
|
653
728
|
void call({ action: 'restore', token: image.data.token, viewportWidth: win.innerWidth, parts }).catch(() => { });
|
|
654
729
|
}
|
|
@@ -689,7 +764,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
689
764
|
.catch(() => { })
|
|
690
765
|
.finally(() => {
|
|
691
766
|
previous.images.forEach((image) => {
|
|
692
|
-
image
|
|
767
|
+
retireImage(image);
|
|
693
768
|
});
|
|
694
769
|
if (retiring !== pending)
|
|
695
770
|
return;
|
|
@@ -762,10 +837,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
762
837
|
},
|
|
763
838
|
consider,
|
|
764
839
|
changed,
|
|
765
|
-
moved:
|
|
766
|
-
updateOffsets();
|
|
767
|
-
followBorderAnimation();
|
|
768
|
-
},
|
|
840
|
+
moved: updateOffsets,
|
|
769
841
|
get token() {
|
|
770
842
|
return ending ? undefined : active?.data.token;
|
|
771
843
|
},
|
|
@@ -783,6 +855,8 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
783
855
|
async destroy() {
|
|
784
856
|
stopped = true;
|
|
785
857
|
invalidate();
|
|
858
|
+
imagePool.forEach((pool) => pool.forEach((image) => image.remove()));
|
|
859
|
+
imagePool.clear();
|
|
786
860
|
await Promise.allSettled([
|
|
787
861
|
bounded(Promise.resolve().then(() => listener?.remove())),
|
|
788
862
|
bounded(Promise.resolve().then(() => eventListener?.remove())),
|