@webview-native-ui/engine 0.0.3 → 0.0.5
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/native/session.d.ts +11 -1
- package/dist/native/session.js +77 -8
- 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.d.ts +3 -1
- package/dist/projection/web.js +273 -176
- 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 +28 -4
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +31 -14
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +20 -12
- package/package.json +1 -1
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`;
|
|
@@ -78,17 +52,48 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
78
52
|
return root.nodeType === 11 && 'host' in root ? root : doc;
|
|
79
53
|
};
|
|
80
54
|
const sheetFor = (root) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
55
|
+
if (sheets.has(root))
|
|
56
|
+
return;
|
|
57
|
+
const style = doc.createElement('style');
|
|
58
|
+
style.setAttribute('data-native-ui-snapshot-base', '');
|
|
59
|
+
style.textContent = snapshotStyles + (root.nodeType === 11 ? snapshotShadowStyles : '');
|
|
60
|
+
(root.nodeType === 11 ? root : root.head).append(style);
|
|
61
|
+
sheets.set(root, style);
|
|
62
|
+
};
|
|
63
|
+
// Each shadow tree is a separate style scope. Keep these rules alongside the
|
|
64
|
+
// prepared image; only the marker changes during the handoff.
|
|
65
|
+
const finishVisibilityTransitions = (target) => {
|
|
66
|
+
// A visibility transition can keep the old content painted over the image.
|
|
67
|
+
// Do not stop layout, transform or opacity transitions owned by the renderer.
|
|
68
|
+
const animations = [
|
|
69
|
+
...target.getAnimations({ subtree: true }),
|
|
70
|
+
...[...(shadowHosts.get(target) ?? [])].flatMap((host) => host.shadowRoot?.getAnimations() ?? []),
|
|
71
|
+
];
|
|
72
|
+
for (const animation of animations)
|
|
73
|
+
if ('transitionProperty' in animation && animation.transitionProperty === 'visibility')
|
|
74
|
+
animation.cancel();
|
|
75
|
+
};
|
|
76
|
+
const shadowHosts = new Map();
|
|
77
|
+
const syncShadowContent = (target) => {
|
|
78
|
+
const hosts = shadowHosts.get(target) ?? new Set();
|
|
79
|
+
shadowHosts.set(target, hosts);
|
|
80
|
+
const hidden = target.hasAttribute('data-native-ui-snapshot-source') || target.hasAttribute('data-native-ui-snapshot-prepared');
|
|
81
|
+
const visit = (element) => {
|
|
82
|
+
if (element.hasAttribute('data-native-ui-projection-node'))
|
|
83
|
+
return;
|
|
84
|
+
if (element.shadowRoot) {
|
|
85
|
+
hosts.add(element);
|
|
86
|
+
sheetFor(element.shadowRoot);
|
|
87
|
+
for (const child of element.shadowRoot.children)
|
|
88
|
+
visit(child);
|
|
89
|
+
}
|
|
90
|
+
for (const child of element.children)
|
|
91
|
+
visit(child);
|
|
92
|
+
};
|
|
93
|
+
visit(target);
|
|
94
|
+
for (const host of hosts)
|
|
95
|
+
host.toggleAttribute('data-native-ui-snapshot-content', hidden);
|
|
96
|
+
finishVisibilityTransitions(target);
|
|
92
97
|
};
|
|
93
98
|
const viewport = () => `${win.innerWidth}:${win.innerHeight}:${win.devicePixelRatio}`;
|
|
94
99
|
let handoffPending = false;
|
|
@@ -96,6 +101,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
96
101
|
let paintFrame = 0;
|
|
97
102
|
let presentation = 0;
|
|
98
103
|
let cached;
|
|
104
|
+
let retiring;
|
|
99
105
|
let active;
|
|
100
106
|
let touching = false;
|
|
101
107
|
let ending = false;
|
|
@@ -108,12 +114,46 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
108
114
|
let stopped = false;
|
|
109
115
|
let flight = false;
|
|
110
116
|
let latest;
|
|
117
|
+
const pruneIdleImages = () => {
|
|
118
|
+
const targets = new Set(latest?.candidates.map((candidate) => candidate.scope.element));
|
|
119
|
+
for (const [target, pool] of imagePool) {
|
|
120
|
+
const reusable = !stopped && target.isConnected && targets.has(target);
|
|
121
|
+
if (reusable && (active || ending || handoffPending))
|
|
122
|
+
continue;
|
|
123
|
+
const limit = reusable ? 2 * Math.max(1, cached?.scopes.filter((scope) => scope.element === target).length ?? 0) : 0;
|
|
124
|
+
let spare = Math.max(0, limit - pool.filter((image) => leasedImages.has(image)).length);
|
|
125
|
+
const retained = pool.filter((image) => {
|
|
126
|
+
if (leasedImages.has(image) || spare-- > 0)
|
|
127
|
+
return true;
|
|
128
|
+
image.remove();
|
|
129
|
+
return false;
|
|
130
|
+
});
|
|
131
|
+
if (retained.length)
|
|
132
|
+
imagePool.set(target, retained);
|
|
133
|
+
else
|
|
134
|
+
imagePool.delete(target);
|
|
135
|
+
}
|
|
136
|
+
const liveRoots = new Set([
|
|
137
|
+
...(latest?.candidates.map((candidate) => candidate.scope.element.getRootNode()) ?? []),
|
|
138
|
+
...(active?.scopes.map((scope) => scope.element.getRootNode()) ?? []),
|
|
139
|
+
...(cached?.scopes.map((scope) => scope.element.getRootNode()) ?? []),
|
|
140
|
+
...[...leasedImages].map((image) => image.getRootNode()),
|
|
141
|
+
...[...shadowHosts.values()].flatMap((hosts) => [...hosts].map((host) => host.shadowRoot)),
|
|
142
|
+
]);
|
|
143
|
+
for (const [root, pair] of sheets)
|
|
144
|
+
if (root.nodeType === 11 && !liveRoots.has(root)) {
|
|
145
|
+
pair.remove();
|
|
146
|
+
sheets.delete(root);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
111
149
|
const pendingCleanup = new Set();
|
|
112
150
|
const cleanup = (action, token, afterPaint) => {
|
|
113
151
|
const run = () => {
|
|
114
152
|
if (!pendingCleanup.delete(run))
|
|
115
153
|
return;
|
|
116
154
|
void call({ action, token }).catch(() => { });
|
|
155
|
+
if (action === 'restored')
|
|
156
|
+
pruneIdleImages();
|
|
117
157
|
};
|
|
118
158
|
pendingCleanup.add(run);
|
|
119
159
|
if (afterPaint && !stopped)
|
|
@@ -130,7 +170,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
130
170
|
if (!item || item === cached || item === active)
|
|
131
171
|
return;
|
|
132
172
|
item.images.forEach((image) => {
|
|
133
|
-
image
|
|
173
|
+
retireImage(image);
|
|
134
174
|
});
|
|
135
175
|
cleanup('release', item.data.token, afterPaint);
|
|
136
176
|
};
|
|
@@ -206,7 +246,19 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
206
246
|
if (result.parts.some((part) => new Set(part.ids.map((id) => selected.find((candidate) => candidate.control.id === id).scope)).size !== 1))
|
|
207
247
|
return;
|
|
208
248
|
await bounded(Promise.all(result.parts.map(async (part) => {
|
|
209
|
-
const
|
|
249
|
+
const target = selected.find((candidate) => part.ids.includes(candidate.control.id)).scope.element;
|
|
250
|
+
let pool = imagePool.get(target);
|
|
251
|
+
if (!pool) {
|
|
252
|
+
pool = [];
|
|
253
|
+
imagePool.set(target, pool);
|
|
254
|
+
}
|
|
255
|
+
let image = pool.find((item) => !leasedImages.has(item));
|
|
256
|
+
if (!image) {
|
|
257
|
+
image = doc.createElement('img');
|
|
258
|
+
image.setAttribute('data-native-ui-projection-node', '');
|
|
259
|
+
pool.push(image);
|
|
260
|
+
}
|
|
261
|
+
leasedImages.add(image);
|
|
210
262
|
images.push(image);
|
|
211
263
|
image.src = projectionURL(part.url);
|
|
212
264
|
await image.decode();
|
|
@@ -223,7 +275,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
223
275
|
selected.some((candidate) => !validTarget(candidate.scope.element) || ![candidate.element, ...(candidate.sources ?? [])].every(validSource)))
|
|
224
276
|
return;
|
|
225
277
|
const previous = cached;
|
|
226
|
-
|
|
278
|
+
const prepared = {
|
|
227
279
|
data: result,
|
|
228
280
|
images,
|
|
229
281
|
sources: Array.from(new Set(selected.flatMap((item) => [item.element, ...(item.sources ?? [])]))),
|
|
@@ -235,12 +287,32 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
235
287
|
revision,
|
|
236
288
|
layout: signature(selected),
|
|
237
289
|
};
|
|
290
|
+
try {
|
|
291
|
+
if (!render(prepared))
|
|
292
|
+
throw new Error('Projection destination is unavailable');
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
// Do not publish a token whose DOM could not be installed. Restore the
|
|
296
|
+
// last completed presentation before retiring the failed preparation.
|
|
297
|
+
detach();
|
|
298
|
+
try {
|
|
299
|
+
render();
|
|
300
|
+
cached?.sources.forEach((source) => {
|
|
301
|
+
preparedOwners.add(source);
|
|
302
|
+
preparedSources.add(source);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
abandon();
|
|
307
|
+
}
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
cached = prepared;
|
|
238
311
|
clearPrepared();
|
|
239
312
|
cached.sources.forEach((source) => {
|
|
240
313
|
preparedOwners.add(source);
|
|
241
314
|
preparedSources.add(source);
|
|
242
315
|
});
|
|
243
|
-
render();
|
|
244
316
|
if (callbacks.opaqueUnderlay)
|
|
245
317
|
void call({ action: 'prepared', token: result.token }).catch(() => { });
|
|
246
318
|
win.dispatchEvent(new Event(sourcesChangedEvent));
|
|
@@ -254,7 +326,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
254
326
|
finally {
|
|
255
327
|
if (!adopted)
|
|
256
328
|
images.forEach((image) => {
|
|
257
|
-
image
|
|
329
|
+
retireImage(image);
|
|
258
330
|
});
|
|
259
331
|
if (result?.token && !adopted)
|
|
260
332
|
void call({ action: 'release', token: result.token }).catch(() => { });
|
|
@@ -290,6 +362,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
290
362
|
if (latest?.revision !== revision)
|
|
291
363
|
generation++;
|
|
292
364
|
latest = { revision, candidates };
|
|
365
|
+
pruneIdleImages();
|
|
293
366
|
if (cached?.revision !== revision || preparedVisualRevision !== visualRevision)
|
|
294
367
|
schedule();
|
|
295
368
|
};
|
|
@@ -303,7 +376,6 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
303
376
|
invalidate();
|
|
304
377
|
if (active) {
|
|
305
378
|
updateOffsets();
|
|
306
|
-
followBorderAnimation();
|
|
307
379
|
}
|
|
308
380
|
visualRevision++;
|
|
309
381
|
generation++;
|
|
@@ -320,17 +392,21 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
320
392
|
}));
|
|
321
393
|
const preparedTargets = new Set();
|
|
322
394
|
const clearTarget = (target) => {
|
|
395
|
+
shadowHosts.get(target)?.forEach((host) => host.removeAttribute('data-native-ui-snapshot-content'));
|
|
323
396
|
if (preparedTargets.delete(target))
|
|
324
397
|
target.removeAttribute('data-native-ui-snapshot-prepared');
|
|
325
398
|
target.removeAttribute('data-native-ui-snapshot-visible');
|
|
326
399
|
target.removeAttribute(imageMarker);
|
|
400
|
+
target.removeAttribute('data-native-ui-snapshot-positioned');
|
|
327
401
|
target.removeAttribute('data-native-ui-snapshot-source');
|
|
402
|
+
finishVisibilityTransitions(target);
|
|
403
|
+
shadowHosts.delete(target);
|
|
328
404
|
};
|
|
329
405
|
const detach = () => {
|
|
330
406
|
presentation++;
|
|
331
407
|
win.cancelAnimationFrame(paintFrame);
|
|
332
|
-
|
|
333
|
-
|
|
408
|
+
for (const element of offsets.keys())
|
|
409
|
+
element.style.setProperty('opacity', '0', 'important');
|
|
334
410
|
offsets.clear();
|
|
335
411
|
paintFrame = 0;
|
|
336
412
|
handoffPending = false;
|
|
@@ -342,11 +418,12 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
342
418
|
});
|
|
343
419
|
rendered.forEach(clearTarget);
|
|
344
420
|
rendered.clear();
|
|
345
|
-
sheets.forEach((
|
|
346
|
-
|
|
347
|
-
|
|
421
|
+
sheets.forEach((style, root) => {
|
|
422
|
+
if (stopped || (root.nodeType === 11 && !root.host.isConnected)) {
|
|
423
|
+
style.remove();
|
|
424
|
+
sheets.delete(root);
|
|
425
|
+
}
|
|
348
426
|
});
|
|
349
|
-
sheets.clear();
|
|
350
427
|
};
|
|
351
428
|
const abandon = (expected = active, afterPaint = ending) => {
|
|
352
429
|
if (active !== expected)
|
|
@@ -370,7 +447,16 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
370
447
|
return;
|
|
371
448
|
}
|
|
372
449
|
const image = active;
|
|
373
|
-
|
|
450
|
+
presentation++;
|
|
451
|
+
win.cancelAnimationFrame(paintFrame);
|
|
452
|
+
paintFrame = 0;
|
|
453
|
+
handoffPending = presented = false;
|
|
454
|
+
image.sources.forEach((source) => {
|
|
455
|
+
owners.delete(source);
|
|
456
|
+
visibleOwners.delete(source);
|
|
457
|
+
});
|
|
458
|
+
rendered.forEach((target) => target.removeAttribute('data-native-ui-snapshot-source'));
|
|
459
|
+
showImages(image, !!callbacks.opaqueUnderlay);
|
|
374
460
|
active = undefined;
|
|
375
461
|
ending = false;
|
|
376
462
|
win.clearTimeout(restoreTimeout);
|
|
@@ -384,7 +470,6 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
384
470
|
preparedOwners.add(source);
|
|
385
471
|
preparedSources.add(source);
|
|
386
472
|
});
|
|
387
|
-
render();
|
|
388
473
|
callbacks.wake();
|
|
389
474
|
};
|
|
390
475
|
const retireRestoration = () => {
|
|
@@ -394,51 +479,60 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
394
479
|
};
|
|
395
480
|
const targetsFor = (image) => image.scopes.map((scope) => (validTarget(scope.element) ? scope.element : undefined));
|
|
396
481
|
const rendered = new Set();
|
|
482
|
+
const showImages = (image, visible) => {
|
|
483
|
+
for (const target of rendered) {
|
|
484
|
+
target.toggleAttribute('data-native-ui-snapshot-visible', visible);
|
|
485
|
+
syncShadowContent(target);
|
|
486
|
+
}
|
|
487
|
+
image.images.forEach((element) => element.style.setProperty('opacity', visible ? '1' : '0', 'important'));
|
|
488
|
+
};
|
|
397
489
|
const preparedKeys = new Map();
|
|
398
490
|
const offsets = new Map();
|
|
399
|
-
let offsetFrame = 0;
|
|
400
491
|
const updateOffsets = () => {
|
|
401
|
-
|
|
402
|
-
|
|
492
|
+
const writes = [];
|
|
493
|
+
// Read all geometry before writing positions; never reset a painted image to zero to measure it.
|
|
494
|
+
for (const [element, placement] of offsets) {
|
|
495
|
+
const { target, frame } = placement;
|
|
496
|
+
if (!target.isConnected) {
|
|
497
|
+
element.remove();
|
|
403
498
|
continue;
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
?.getKeyframes()
|
|
421
|
-
.some((frame) => ['borderLeftWidth', 'borderTopWidth', 'borderWidth'].some((property) => property in frame))));
|
|
422
|
-
if (movingBorder)
|
|
423
|
-
offsetFrame = win.requestAnimationFrame(() => {
|
|
424
|
-
offsetFrame = 0;
|
|
425
|
-
updateOffsets();
|
|
426
|
-
followBorderAnimation();
|
|
499
|
+
}
|
|
500
|
+
const parent = target.shadowRoot ?? target;
|
|
501
|
+
if (element.parentNode !== parent)
|
|
502
|
+
parent.insertBefore(element, null);
|
|
503
|
+
const box = target.getBoundingClientRect();
|
|
504
|
+
const rect = element.getBoundingClientRect();
|
|
505
|
+
const width = parseFloat(element.style.width) || frame.width;
|
|
506
|
+
const height = parseFloat(element.style.height) || frame.height;
|
|
507
|
+
const sx = rect.width / width || 1, sy = rect.height / height || 1;
|
|
508
|
+
const initial = placement.scale ?? (placement.scale = { x: sx, y: sy });
|
|
509
|
+
writes.push({
|
|
510
|
+
element,
|
|
511
|
+
left: (parseFloat(element.style.left) || 0) + (box.x + (frame.x * sx) / initial.x - rect.x) / sx,
|
|
512
|
+
top: (parseFloat(element.style.top) || 0) + (box.y + (frame.y * sy) / initial.y - rect.y) / sy,
|
|
513
|
+
width: frame.width / initial.x,
|
|
514
|
+
height: frame.height / initial.y,
|
|
427
515
|
});
|
|
516
|
+
}
|
|
517
|
+
for (const { element, ...values } of writes)
|
|
518
|
+
for (const [property, value] of Object.entries(values)) {
|
|
519
|
+
const next = `${value}px`;
|
|
520
|
+
if (element.style.getPropertyValue(property) !== next)
|
|
521
|
+
element.style.setProperty(property, next, 'important');
|
|
522
|
+
}
|
|
428
523
|
};
|
|
429
|
-
const render = () => {
|
|
430
|
-
const image = active ?? cached;
|
|
524
|
+
const render = (image = active ?? cached) => {
|
|
431
525
|
if (!image || ending)
|
|
432
|
-
return;
|
|
526
|
+
return false;
|
|
433
527
|
if (!image.sources.every(validSource) || targetsFor(image).some((target) => !target)) {
|
|
434
528
|
invalidate();
|
|
435
|
-
return;
|
|
529
|
+
return false;
|
|
436
530
|
}
|
|
437
531
|
const targets = targetsFor(image);
|
|
438
532
|
if (active) {
|
|
439
533
|
if (targets.some((target) => !target?.isConnected || !sheets.has(rootFor(target)))) {
|
|
440
534
|
abandon(active, false);
|
|
441
|
-
return;
|
|
535
|
+
return false;
|
|
442
536
|
}
|
|
443
537
|
const next = new Set();
|
|
444
538
|
for (const [index, source] of image.targets.entries()) {
|
|
@@ -448,7 +542,6 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
448
542
|
continue;
|
|
449
543
|
target.setAttribute(imageMarker, key);
|
|
450
544
|
target.setAttribute('data-native-ui-snapshot-source', '');
|
|
451
|
-
target.toggleAttribute('data-native-ui-snapshot-visible', presented);
|
|
452
545
|
next.add(target);
|
|
453
546
|
}
|
|
454
547
|
for (const target of rendered)
|
|
@@ -456,83 +549,65 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
456
549
|
clearTarget(target);
|
|
457
550
|
rendered.clear();
|
|
458
551
|
next.forEach((target) => rendered.add(target));
|
|
459
|
-
|
|
552
|
+
showImages(image, presented);
|
|
553
|
+
return true;
|
|
460
554
|
}
|
|
555
|
+
imagePool.forEach((pool) => pool.forEach((element) => {
|
|
556
|
+
if (!image.images.includes(element)) {
|
|
557
|
+
element.style.setProperty('display', 'none', 'important');
|
|
558
|
+
element.removeAttribute('data-native-ui-projection-image');
|
|
559
|
+
}
|
|
560
|
+
}));
|
|
461
561
|
preparedKeys.clear();
|
|
462
562
|
offsets.clear();
|
|
463
563
|
rendered.forEach(clearTarget);
|
|
464
564
|
rendered.clear();
|
|
465
|
-
// Reuse the image stylesheet. Presentation states belong to the runtime's
|
|
466
|
-
// permanent CSS, which survives cache replacement and image retirement.
|
|
467
|
-
const groups = new Map();
|
|
468
565
|
for (const target of new Set(targets)) {
|
|
469
566
|
if (!target)
|
|
470
567
|
continue;
|
|
471
568
|
rendered.add(target);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
const { rules, positions } = group;
|
|
479
|
-
// A decoded clone stays concealed even if this cache is invalidated before
|
|
480
|
-
// the projection adapter publishes its next Native/Web ownership state.
|
|
481
|
-
if (target !== image.targets[targets.indexOf(target)] && !target.hasAttribute('data-native-ui-snapshot-prepared')) {
|
|
569
|
+
sheetFor(rootFor(target));
|
|
570
|
+
const indices = targets.flatMap((owner, index) => (owner === target ? [index] : []));
|
|
571
|
+
const key = `${imageIdentity}-${indices[0]}`;
|
|
572
|
+
target.setAttribute(imageMarker, key);
|
|
573
|
+
if (target !== image.targets[indices[0]]) {
|
|
482
574
|
target.setAttribute('data-native-ui-snapshot-prepared', '');
|
|
483
575
|
preparedTargets.add(target);
|
|
484
576
|
}
|
|
485
|
-
target.toggleAttribute('data-native-ui-snapshot-visible', !!
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
{
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
577
|
+
target.toggleAttribute('data-native-ui-snapshot-visible', !!callbacks.opaqueUnderlay);
|
|
578
|
+
syncShadowContent(target);
|
|
579
|
+
const box = target.getBoundingClientRect();
|
|
580
|
+
target.toggleAttribute('data-native-ui-snapshot-positioned', win.getComputedStyle(target).position === 'static');
|
|
581
|
+
const parent = target.shadowRoot ?? target;
|
|
582
|
+
for (const index of [...indices].reverse()) {
|
|
583
|
+
const element = image.images[index];
|
|
584
|
+
const part = image.data.parts[index];
|
|
585
|
+
const frame = image.frames[index] ?? {
|
|
586
|
+
x: callbacks.opaqueUnderlay ? 0 : part.x - box.x,
|
|
587
|
+
y: callbacks.opaqueUnderlay ? 0 : part.y - box.y,
|
|
588
|
+
width: part.width,
|
|
589
|
+
height: part.height,
|
|
590
|
+
};
|
|
591
|
+
image.frames[index] = frame;
|
|
592
|
+
preparedKeys.set(image.targets[index], key);
|
|
593
|
+
element.inert = true;
|
|
594
|
+
element.setAttribute('aria-hidden', 'true');
|
|
595
|
+
element.setAttribute('data-native-ui-projection-image', key);
|
|
596
|
+
element.style.cssText =
|
|
597
|
+
'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;';
|
|
598
|
+
element.style.setProperty('width', `${frame.width}px`, 'important');
|
|
599
|
+
element.style.setProperty('height', `${frame.height}px`, 'important');
|
|
600
|
+
element.style.setProperty('left', '0px', 'important');
|
|
601
|
+
element.style.setProperty('top', '0px', 'important');
|
|
602
|
+
element.style.setProperty('opacity', callbacks.opaqueUnderlay ? '1' : '0', 'important');
|
|
603
|
+
if (element.parentNode !== parent)
|
|
604
|
+
parent.insertBefore(element, null);
|
|
605
|
+
offsets.set(element, { target, frame });
|
|
503
606
|
}
|
|
504
|
-
const frames = indices.map((index) => image.frames[index]);
|
|
505
|
-
const x = Math.min(...frames.map((frame) => frame.x));
|
|
506
|
-
const y = Math.min(...frames.map((frame) => frame.y));
|
|
507
|
-
const right = Math.max(...frames.map((frame) => frame.x + frame.width));
|
|
508
|
-
const bottom = Math.max(...frames.map((frame) => frame.y + frame.height));
|
|
509
|
-
const left = (parseFloat(style.borderLeftWidth) || 0) - target.scrollLeft;
|
|
510
|
-
const top = (parseFloat(style.borderTopWidth) || 0) - target.scrollTop;
|
|
511
|
-
const key = `${imageIdentity}-${indices[0]}`;
|
|
512
|
-
indices.forEach((index) => preparedKeys.set(image.targets[index], key));
|
|
513
|
-
target.setAttribute(imageMarker, key);
|
|
514
|
-
rules.push(`[${imageMarker}="${key}"][data-native-ui-snapshot-visible] { ${style.position === 'static' ? 'position:relative!important;' : ''} }`);
|
|
515
|
-
const backgrounds = indices.map((index) => {
|
|
516
|
-
const frame = image.frames[index];
|
|
517
|
-
return `url(${JSON.stringify(image.images[index].src)}) ${frame.x - x}px ${frame.y - y}px / ${frame.width}px ${frame.height}px no-repeat`;
|
|
518
|
-
});
|
|
519
|
-
const declaration = doc.createElement('span').style;
|
|
520
|
-
declaration.cssText = `left:${x - left}px;top:${y - top}px;width:${right - x}px;height:${bottom - y}px;background:${backgrounds.join(',')};`;
|
|
521
|
-
for (const property of Array.from(declaration))
|
|
522
|
-
declaration.setProperty(property, declaration.getPropertyValue(property), 'important');
|
|
523
|
-
// Outrank every manager's shared reset, even when another manager starts later.
|
|
524
|
-
rules.push(`[${imageMarker}="${key}"][${imageMarker}][${imageMarker}]::before { ${declaration.cssText} }`);
|
|
525
|
-
positions.push({ target, x, y });
|
|
526
607
|
}
|
|
527
608
|
image.parents = image.scopes.map((scope) => scope.element.parentNode);
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
positions.forEach(({ target, x, y }, index) => offsets.set(target, {
|
|
531
|
-
x,
|
|
532
|
-
y,
|
|
533
|
-
style: sheet.sheet.cssRules[index * 2 + 1].style,
|
|
534
|
-
}));
|
|
535
|
-
}
|
|
609
|
+
updateOffsets();
|
|
610
|
+
return true;
|
|
536
611
|
};
|
|
537
612
|
const begin = () => {
|
|
538
613
|
retireRestoration();
|
|
@@ -564,10 +639,8 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
564
639
|
image.sources.forEach((source) => {
|
|
565
640
|
owners.add(source);
|
|
566
641
|
});
|
|
567
|
-
rendered.forEach((target) =>
|
|
568
|
-
|
|
569
|
-
target.toggleAttribute('data-native-ui-snapshot-visible', presented);
|
|
570
|
-
});
|
|
642
|
+
rendered.forEach((target) => target.setAttribute('data-native-ui-snapshot-source', ''));
|
|
643
|
+
showImages(image, presented);
|
|
571
644
|
// Reconciliation may continue after one frame even if the optional bridge reply
|
|
572
645
|
// is delayed. The ordered native transport processes cover before that update.
|
|
573
646
|
paintFrame = win.requestAnimationFrame(() => {
|
|
@@ -593,7 +666,6 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
593
666
|
image.sources.forEach((source) => visibleOwners.add(source));
|
|
594
667
|
render();
|
|
595
668
|
updateOffsets();
|
|
596
|
-
followBorderAnimation();
|
|
597
669
|
}
|
|
598
670
|
win.cancelAnimationFrame(paintFrame);
|
|
599
671
|
paintFrame = win.requestAnimationFrame(() => {
|
|
@@ -624,6 +696,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
624
696
|
}
|
|
625
697
|
if (callbacks.blocked())
|
|
626
698
|
return;
|
|
699
|
+
updateOffsets();
|
|
627
700
|
if (callbacks.transient) {
|
|
628
701
|
const image = active;
|
|
629
702
|
const targets = targetsFor(image);
|
|
@@ -632,15 +705,8 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
632
705
|
return;
|
|
633
706
|
}
|
|
634
707
|
const parts = targets.map((target, index) => {
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
return {
|
|
638
|
-
...image.data.parts[index],
|
|
639
|
-
x: space.x + frame.x * space.scaleX,
|
|
640
|
-
y: space.y + frame.y * space.scaleY,
|
|
641
|
-
width: frame.width * space.scaleX,
|
|
642
|
-
height: frame.height * space.scaleY,
|
|
643
|
-
};
|
|
708
|
+
const rect = image.images[index].getBoundingClientRect();
|
|
709
|
+
return { ...image.data.parts[index], x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
644
710
|
});
|
|
645
711
|
if (parts.some((part, index) => ['x', 'y', 'width', 'height'].some((key) => part[key] !== image.data.parts[index][key]))) {
|
|
646
712
|
if (callbacks.opaqueUnderlay)
|
|
@@ -672,12 +738,44 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
672
738
|
finishRestoration();
|
|
673
739
|
schedule();
|
|
674
740
|
};
|
|
675
|
-
const invalidate = () => {
|
|
741
|
+
const invalidate = (nativeHidden = false) => {
|
|
676
742
|
pendingCleanup.forEach((run) => run());
|
|
677
743
|
generation++;
|
|
678
744
|
win.clearTimeout(timer);
|
|
679
745
|
const previous = cached;
|
|
680
746
|
cached = undefined;
|
|
747
|
+
if (nativeHidden || stopped)
|
|
748
|
+
retiring = undefined;
|
|
749
|
+
if (callbacks.retainPreparedUntilRelease && !active && !nativeHidden && !stopped) {
|
|
750
|
+
if (previous) {
|
|
751
|
+
const pending = {};
|
|
752
|
+
retiring = pending;
|
|
753
|
+
void bounded(call({ action: 'release', token: previous.data.token }))
|
|
754
|
+
.catch(() => { })
|
|
755
|
+
.finally(() => {
|
|
756
|
+
previous.images.forEach((image) => {
|
|
757
|
+
retireImage(image);
|
|
758
|
+
});
|
|
759
|
+
if (retiring !== pending)
|
|
760
|
+
return;
|
|
761
|
+
retiring = undefined;
|
|
762
|
+
if (cached || active)
|
|
763
|
+
return;
|
|
764
|
+
// Another session can already own the same host; retire only our own DOM markers.
|
|
765
|
+
for (const target of rendered) {
|
|
766
|
+
if (target.getAttribute(imageMarker) !== preparedKeys.get(target)) {
|
|
767
|
+
rendered.delete(target);
|
|
768
|
+
preparedTargets.delete(target);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
detach();
|
|
772
|
+
preparedKeys.clear();
|
|
773
|
+
win.dispatchEvent(new Event(sourcesChangedEvent));
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
if (retiring)
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
681
779
|
dispose(previous);
|
|
682
780
|
if (active)
|
|
683
781
|
abandon(active, false);
|
|
@@ -729,10 +827,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
729
827
|
},
|
|
730
828
|
consider,
|
|
731
829
|
changed,
|
|
732
|
-
moved:
|
|
733
|
-
updateOffsets();
|
|
734
|
-
followBorderAnimation();
|
|
735
|
-
},
|
|
830
|
+
moved: updateOffsets,
|
|
736
831
|
get token() {
|
|
737
832
|
return ending ? undefined : active?.data.token;
|
|
738
833
|
},
|
|
@@ -750,6 +845,8 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
750
845
|
async destroy() {
|
|
751
846
|
stopped = true;
|
|
752
847
|
invalidate();
|
|
848
|
+
imagePool.forEach((pool) => pool.forEach((image) => image.remove()));
|
|
849
|
+
imagePool.clear();
|
|
753
850
|
await Promise.allSettled([
|
|
754
851
|
bounded(Promise.resolve().then(() => listener?.remove())),
|
|
755
852
|
bounded(Promise.resolve().then(() => eventListener?.remove())),
|
|
@@ -828,7 +925,7 @@ export const createWebProjectionBatch = async (transport, options) => {
|
|
|
828
925
|
end: () => engine?.end(),
|
|
829
926
|
updated: (token, ids, revision) => engine?.updated(token, ids, revision),
|
|
830
927
|
retireRestoration: () => engine?.retireRestoration(),
|
|
831
|
-
invalidate: () => engine?.invalidate(),
|
|
928
|
+
invalidate: (nativeHidden) => engine?.invalidate(nativeHidden),
|
|
832
929
|
async destroy() {
|
|
833
930
|
stopped = true;
|
|
834
931
|
await initializing;
|