@webview-native-ui/engine 0.0.6 → 0.0.8
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 +19 -12
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/native/session.d.ts +5 -0
- package/dist/native/session.js +125 -16
- package/dist/native/updates.d.ts +16 -0
- package/dist/native/updates.js +108 -0
- package/dist/native/values.d.ts +5 -0
- package/dist/native/values.js +27 -0
- package/dist/projection/surface.d.ts +35 -0
- package/dist/projection/surface.js +81 -0
- package/dist/projection/surfaces.d.ts +20 -0
- package/dist/projection/surfaces.js +116 -0
- package/docs/README.md +70 -0
- package/docs/advanced/README.md +20 -0
- package/docs/advanced/android-placement.md +48 -0
- package/docs/advanced/native-handoff.md +89 -0
- package/docs/advanced/renderer.md +148 -0
- package/docs/advanced/update-queue.md +58 -0
- package/docs/advanced/web-projection.md +88 -0
- package/docs/assets/ios-native-capture-comparison.png +0 -0
- package/docs/concepts.md +97 -0
- package/docs/configuration/README.md +29 -0
- package/docs/configuration/capture.md +27 -0
- package/docs/configuration/events.md +35 -0
- package/docs/configuration/ionic-overlays.md +65 -0
- package/docs/configuration/values.md +125 -0
- package/docs/cordova.md +1 -1
- package/docs/{capacitor-plugin.md → getting-started/capacitor.md} +45 -47
- package/docs/{cordova-plugin.md → getting-started/cordova.md} +32 -15
- package/docs/getting-started/initialization.md +79 -0
- package/docs/maintenance.md +38 -8
- package/docs/{projection-containers.md → reference/containers.md} +6 -2
- package/docs/reference/platforms.md +137 -0
- package/docs/reference/projection.md +134 -0
- package/docs/reference/state.md +39 -0
- package/ios/Sources/WebViewNativeUIEngine/ClippedView.swift +12 -12
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +68 -7
- package/ios/Sources/WebViewNativeUIEngine/Frame.swift +10 -10
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +74 -39
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +6 -6
- package/ios/Sources/WebViewNativeUIEngine/ScrollBindings.swift +2 -0
- package/ios/Sources/WebViewNativeUIEngine/ScrollTracker.swift +9 -7
- package/package.json +9 -4
- package/docs/projection.md +0 -368
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** Owns asynchronous projection creation and disposal for DOM-backed surfaces. */
|
|
2
|
+
export const createWebProjectionSurfaces = (options = {}) => {
|
|
3
|
+
const entries = new Map();
|
|
4
|
+
let stopped = false;
|
|
5
|
+
const values = () => [...entries.values()].flatMap((entry) => (entry.surface ? [entry.surface] : []));
|
|
6
|
+
let doc;
|
|
7
|
+
let listeners;
|
|
8
|
+
let motionFrame = 0;
|
|
9
|
+
const invalidate = () => values().forEach((surface) => surface.invalidate());
|
|
10
|
+
const changed = (owner) => values().forEach((surface) => surface.changed(owner));
|
|
11
|
+
const observe = (owner) => {
|
|
12
|
+
const ownerDocument = owner.nodeType === 9 ? owner : owner.ownerDocument;
|
|
13
|
+
if (doc && doc !== ownerDocument)
|
|
14
|
+
throw new TypeError('Projection surfaces must share one Document');
|
|
15
|
+
if (doc)
|
|
16
|
+
return;
|
|
17
|
+
doc = ownerDocument;
|
|
18
|
+
const win = doc.defaultView;
|
|
19
|
+
listeners = new win.AbortController();
|
|
20
|
+
const subscription = { signal: listeners.signal };
|
|
21
|
+
win.addEventListener('resize', invalidate, subscription);
|
|
22
|
+
win.addEventListener('orientationchange', invalidate, subscription);
|
|
23
|
+
doc.addEventListener('visibilitychange', () => {
|
|
24
|
+
if (doc.hidden)
|
|
25
|
+
invalidate();
|
|
26
|
+
}, subscription);
|
|
27
|
+
const appearance = (event) => {
|
|
28
|
+
const target = event.target instanceof win.Element ? event.target : undefined;
|
|
29
|
+
if (target?.closest('style[data-native-ui-snapshot], [data-native-ui-projection-node]') || (target && options.ignoreTarget?.(target)))
|
|
30
|
+
return;
|
|
31
|
+
if (target?.hasAttribute('data-native-ui-snapshot') && event.pseudoElement)
|
|
32
|
+
return;
|
|
33
|
+
changed();
|
|
34
|
+
};
|
|
35
|
+
for (const name of ['load', 'transitionrun', 'transitionend', 'transitioncancel', 'animationstart', 'animationend', 'animationcancel'])
|
|
36
|
+
doc.addEventListener(name, appearance, { ...subscription, capture: name === 'load' });
|
|
37
|
+
};
|
|
38
|
+
const followMotion = (moving, target = (source) => source) => {
|
|
39
|
+
if (motionFrame || stopped || !moving.size || !doc?.defaultView)
|
|
40
|
+
return;
|
|
41
|
+
// Ancestor transforms already carry the image. Only the host's own motion needs rebasing.
|
|
42
|
+
const surfaces = values().filter((surface) => (surface.token || surface.restoring) && surface.owners.some((owner) => moving.has(target(owner))));
|
|
43
|
+
if (!surfaces.length)
|
|
44
|
+
return;
|
|
45
|
+
motionFrame = doc.defaultView.requestAnimationFrame(() => {
|
|
46
|
+
motionFrame = 0;
|
|
47
|
+
surfaces.forEach((surface) => surface.moved());
|
|
48
|
+
followMotion(moving, target);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const remove = async (owner) => {
|
|
52
|
+
const entry = entries.get(owner);
|
|
53
|
+
if (!entry)
|
|
54
|
+
return;
|
|
55
|
+
entries.delete(owner);
|
|
56
|
+
if (entry.surface)
|
|
57
|
+
await entry.surface.destroy();
|
|
58
|
+
// A failed creation has nothing to dispose; ensure() reports that error.
|
|
59
|
+
else
|
|
60
|
+
await entry.ready.catch(() => { });
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
get: (owner) => entries.get(owner)?.surface,
|
|
64
|
+
keys: () => entries.keys(),
|
|
65
|
+
values,
|
|
66
|
+
changed,
|
|
67
|
+
invalidate,
|
|
68
|
+
followMotion,
|
|
69
|
+
end: () => values().forEach((surface) => surface.end()),
|
|
70
|
+
consider: (...args) => values().forEach((surface) => surface.consider(...args)),
|
|
71
|
+
updated: (...args) => values().forEach((surface) => surface.updated(...args)),
|
|
72
|
+
get restoring() {
|
|
73
|
+
return values().some((surface) => surface.restoring);
|
|
74
|
+
},
|
|
75
|
+
get handoffPending() {
|
|
76
|
+
return values().some((surface) => surface.handoffPending);
|
|
77
|
+
},
|
|
78
|
+
ensure(owner, create) {
|
|
79
|
+
if (stopped || !owner.isConnected)
|
|
80
|
+
return Promise.resolve(undefined);
|
|
81
|
+
observe(owner);
|
|
82
|
+
const existing = entries.get(owner);
|
|
83
|
+
if (existing)
|
|
84
|
+
return existing.ready;
|
|
85
|
+
const entry = {
|
|
86
|
+
ready: Promise.resolve()
|
|
87
|
+
.then(create)
|
|
88
|
+
.then(async (surface) => {
|
|
89
|
+
if (stopped || entries.get(owner) !== entry || !owner.isConnected) {
|
|
90
|
+
if (entries.get(owner) === entry)
|
|
91
|
+
entries.delete(owner);
|
|
92
|
+
await surface.destroy();
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
entry.surface = surface;
|
|
96
|
+
return surface;
|
|
97
|
+
})
|
|
98
|
+
.catch((error) => {
|
|
99
|
+
if (entries.get(owner) === entry)
|
|
100
|
+
entries.delete(owner);
|
|
101
|
+
throw error;
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
entries.set(owner, entry);
|
|
105
|
+
return entry.ready;
|
|
106
|
+
},
|
|
107
|
+
delete: remove,
|
|
108
|
+
async destroy() {
|
|
109
|
+
stopped = true;
|
|
110
|
+
listeners?.abort();
|
|
111
|
+
if (motionFrame)
|
|
112
|
+
doc?.defaultView?.cancelAnimationFrame(motionFrame);
|
|
113
|
+
await Promise.all([...entries.keys()].map(remove));
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
};
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
Start with the smallest integration that meets your component's needs. Basic
|
|
4
|
+
registration is enough for a single native view; optional settings adapt it to
|
|
5
|
+
changing content and application lifecycle. Advanced APIs are a separate route for
|
|
6
|
+
renderers that already own batching or placement.
|
|
7
|
+
|
|
8
|
+
## Choose by the problem you need to solve
|
|
9
|
+
|
|
10
|
+
| Case | Start here | Why |
|
|
11
|
+
| ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
12
|
+
| A Web modal must cover a plugin's native tab bar | [Basic registration](getting-started/initialization.md), then the framework integration guide | Register the view and its DOM host once; the Engine owns capture and restoration. |
|
|
13
|
+
| A hidden fallback returns with the old toggle selection | [Value bindings](configuration/values.md) | Reflect current native data on the fallback host, independently of where it is displayed. |
|
|
14
|
+
| Background toolbar buttons and foreground modal buttons need different handoff lifetimes | [Renderer surfaces](advanced/renderer.md) | Separate owners so closing one surface cannot restore another's controls. |
|
|
15
|
+
| An existing renderer overlaps updates or loses a native acknowledgement | [Update queue](advanced/update-queue.md) | Serialize fresh-state passes and reconcile the connection without replaying user actions. |
|
|
16
|
+
|
|
17
|
+
The full reading order follows. Skip advanced APIs unless your renderer owns the
|
|
18
|
+
work described in their cases.
|
|
19
|
+
|
|
20
|
+
## 1. Understand the approach
|
|
21
|
+
|
|
22
|
+
[Concepts and limits](concepts.md) explains what projection makes possible, how it
|
|
23
|
+
differs from live native UI and Web fallback, and which responsibilities stay with
|
|
24
|
+
the plugin.
|
|
25
|
+
|
|
26
|
+
## 2. Initialize and integrate
|
|
27
|
+
|
|
28
|
+
Read [basic initialization](getting-started/initialization.md) for the minimum call
|
|
29
|
+
and required inputs. Then choose one platform-framework guide:
|
|
30
|
+
|
|
31
|
+
- [Capacitor plugin](getting-started/capacitor.md): dependencies, native plugin hooks, event forwarding and teardown.
|
|
32
|
+
- [Cordova plugin](getting-started/cordova.md): the same registration with Cordova startup and resource handling.
|
|
33
|
+
|
|
34
|
+
Neither route requires a second application-side Engine wrapper or the advanced
|
|
35
|
+
renderer APIs.
|
|
36
|
+
|
|
37
|
+
## 3. Add optional behavior
|
|
38
|
+
|
|
39
|
+
[Registration settings](configuration/README.md) lists defaults and the situations
|
|
40
|
+
that require each option. Read only the relevant detail:
|
|
41
|
+
|
|
42
|
+
- [Notifications](configuration/events.md): tell the Engine when appearance or presentation changes.
|
|
43
|
+
- [Value bindings](configuration/values.md): keep the fallback host's component data aligned with native state.
|
|
44
|
+
- [Capture timing](configuration/capture.md): prepare pixels after native appearance settles.
|
|
45
|
+
- [Ionic overlays](configuration/ionic-overlays.md): connect Ionic coverage with one shared application initializer.
|
|
46
|
+
|
|
47
|
+
## 4. Use advanced APIs when you own a renderer
|
|
48
|
+
|
|
49
|
+
[Choose an advanced API](advanced/README.md) before adding one. These guides address
|
|
50
|
+
different existing renderer needs; they are not a sequence of required setup steps.
|
|
51
|
+
|
|
52
|
+
- [Page and modal surfaces](advanced/renderer.md)
|
|
53
|
+
- [Low-level Web projection and batches](advanced/web-projection.md)
|
|
54
|
+
- [Native handoff and scroll bindings](advanced/native-handoff.md)
|
|
55
|
+
- [Deferred Android placement](advanced/android-placement.md)
|
|
56
|
+
- [Update serialization and recovery](advanced/update-queue.md)
|
|
57
|
+
|
|
58
|
+
## Reference
|
|
59
|
+
|
|
60
|
+
Use these to check contracts and supported behavior while implementing either route:
|
|
61
|
+
|
|
62
|
+
- [Projection contract and lifetime](reference/projection.md)
|
|
63
|
+
- [HTML and Ionic container support](reference/containers.md)
|
|
64
|
+
- [Observable presentation intent](reference/state.md)
|
|
65
|
+
- [Capture and platform limits](reference/platforms.md)
|
|
66
|
+
|
|
67
|
+
## Contributing
|
|
68
|
+
|
|
69
|
+
[Development and releases](maintenance.md) covers repository structure, local
|
|
70
|
+
verification, CI and publication. It is not required for consuming the Engine.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Advanced APIs
|
|
2
|
+
|
|
3
|
+
The basic registration already owns measurement, capture and handoff acknowledgement.
|
|
4
|
+
Do not add a second projection manager for the same host. Use the following APIs
|
|
5
|
+
when your renderer already owns the corresponding work and needs to coordinate it:
|
|
6
|
+
|
|
7
|
+
| Need | API / guide |
|
|
8
|
+
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| Apply deferred native placement just before an opaque Android view returns | `beforeNativeReveal`, `updatePlacement`, `isProjecting` and `bindProjectionPlacement`; see [Android placement handoff](android-placement.md#final-placement-when-returning-from-an-opaque-image-android). Both callbacks default to no custom placement work. |
|
|
10
|
+
| Manage independent pages or modals containing multiple native controls | `createWebProjectionSurface` / `createWebProjectionSurfaces`; see the [renderer guide](renderer.md). |
|
|
11
|
+
| Coordinate several fallback containers in one native transaction | `createWebProjectionBatch`; see [batch handoff](web-projection.md#multiple-fallbacks-in-one-native-transaction). |
|
|
12
|
+
| Fit an existing ownership model that surface helpers cannot express | `createWebProjectionScope` / `createWebProjection`; see [low-level projection](web-projection.md). |
|
|
13
|
+
| Serialize replaceable state updates and recover a lost acknowledgement | `createNativeUpdateQueue`; see [update scheduling and recovery](update-queue.md). |
|
|
14
|
+
| Implement the native bridge for a custom renderer | Snapshot-manager `handle` methods; see [native handoff](native-handoff.md). |
|
|
15
|
+
|
|
16
|
+
These are integration choices, not additional steps to complete a normal
|
|
17
|
+
`NativeUIProjection` setup. Keep business actions, SDK interaction and application
|
|
18
|
+
notifications in the consuming plugin.
|
|
19
|
+
|
|
20
|
+
[Documentation overview](../README.md)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Deferred placement on Android
|
|
2
|
+
|
|
3
|
+
Use this only when an existing renderer defers native placement while an opaque
|
|
4
|
+
image owns presentation. Basic registrations do not require these callbacks.
|
|
5
|
+
|
|
6
|
+
For example, an opaque native advertisement moves through a scrolling feed.
|
|
7
|
+
Once its image follows the DOM, repeatedly laying out the hidden native view adds
|
|
8
|
+
work without changing visible pixels. Defer that placement and apply the latest
|
|
9
|
+
position before revealing native content, so it does not jump back to the old
|
|
10
|
+
location. Keep ordinary placement tracking if the view cannot use an opaque image.
|
|
11
|
+
|
|
12
|
+
Android VIEW consumers can read `isProjecting` on the UI thread after a successful
|
|
13
|
+
cover to defer same-size native placement work. Keep only the latest accepted placement;
|
|
14
|
+
continue validating sequence/generation, and process hide, resize and teardown immediately.
|
|
15
|
+
Supply `beforeNativeReveal` to apply that pending placement synchronously before normal
|
|
16
|
+
restoration, failure recovery, or active-image release reveals native pixels. The engine
|
|
17
|
+
clears `isProjecting` before invoking the callback, so applying placement must not defer
|
|
18
|
+
again. The callback may run without pending work and must be idempotent. Clear consumer
|
|
19
|
+
pending work on hide/destroy and validate the loaded View identity before applying it.
|
|
20
|
+
The callback does not replace content/size invalidation or JavaScript geometry tracking.
|
|
21
|
+
|
|
22
|
+
### Final placement when returning from an opaque image (Android)
|
|
23
|
+
|
|
24
|
+
A plugin that normally measures and posts native placement on every scroll can opt
|
|
25
|
+
into a final placement handoff. Pass `updatePlacement: (JSONObject) -> Unit` to its
|
|
26
|
+
Android `NativeUIProjection` registration and bind its Web host with
|
|
27
|
+
`bindProjectionPlacement(element, measure, invalidate)`.
|
|
28
|
+
|
|
29
|
+
- `binding.projected` becomes true only after native confirms the image handoff.
|
|
30
|
+
While true, the plugin can reuse that host's last placement in full placement
|
|
31
|
+
batches and skip measuring it. Other hosts continue their ordinary updates.
|
|
32
|
+
- `measure()` returns a JSON object synchronously. The engine includes it in the
|
|
33
|
+
existing restore request (or an active-image recovery request). Native validates
|
|
34
|
+
the active image token and View identity, calls `updatePlacement`, then reveals
|
|
35
|
+
native and acknowledges its draw. No extra bridge round trip is introduced.
|
|
36
|
+
- The plugin must validate its own identity, generation and per-host sequence in
|
|
37
|
+
`updatePlacement`. Throw to reject a stale or invalid placement. A final update
|
|
38
|
+
for one host must not cause updates for another host to be dropped.
|
|
39
|
+
- `invalidate()` is called after a failed handoff request. Clear optimistic
|
|
40
|
+
placement/deduplication caches and schedule ordinary placement reconciliation.
|
|
41
|
+
- Dispose the binding when detaching a host. Session ownership prevents an old
|
|
42
|
+
registration's delayed acknowledgement from clearing a new registration's
|
|
43
|
+
projection state. Pause, author visibility and layout changes must still use
|
|
44
|
+
the plugin's normal visibility/invalidation paths.
|
|
45
|
+
|
|
46
|
+
This opt-in does not change registrations without `updatePlacement`. Android rejects
|
|
47
|
+
`updatePlacement` unless `captureMode = CaptureMode.VIEW`; live video and other ineligible captures
|
|
48
|
+
continue native placement tracking under `failedFallback = NATIVE`.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Native handoff for custom renderers
|
|
2
|
+
|
|
3
|
+
An existing renderer should forward its ordered `WebProjectionRequest` messages
|
|
4
|
+
to the native snapshot manager's `handle` method. This keeps request validation,
|
|
5
|
+
CSS-to-native coordinate conversion, cached image handoff and copy retirement in
|
|
6
|
+
the Engine. The bridge remains the plugin's own Capacitor or Cordova method; the
|
|
7
|
+
Engine does not depend on either framework's call object.
|
|
8
|
+
|
|
9
|
+
For example, a renderer already sends toolbar buttons as one native update and
|
|
10
|
+
knows which views belong to each button group. Forward image handoff messages to
|
|
11
|
+
`handle` instead of reimplementing coordinate conversion and copy retirement for
|
|
12
|
+
each group. Keep the group's capture eligibility and ordinary layout in that
|
|
13
|
+
renderer: the Engine cannot infer which sibling views may safely hide together.
|
|
14
|
+
|
|
15
|
+
Call view and snapshot lifecycle APIs on the main/UI thread. Supply two renderer
|
|
16
|
+
callbacks: `frames(ids, revision)` returns current eligible bounds, and `views(ids)`
|
|
17
|
+
returns the complete native views that can safely be hidden together. Return empty
|
|
18
|
+
bounds or `nil` / `null` when that operation is unavailable. Bounds use **points
|
|
19
|
+
relative to the WebView on iOS** and **physical pixels relative to the WebView on
|
|
20
|
+
Android**. The Engine checks that measured IDs exactly match the request.
|
|
21
|
+
|
|
22
|
+
```swift
|
|
23
|
+
snapshots.handle(request, in: webView,
|
|
24
|
+
frames: { ids, revision in renderer.measure(ids, at: revision) },
|
|
25
|
+
views: { ids in renderer.viewsAllowedToHide(ids) },
|
|
26
|
+
complete: reply)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
snapshots.handle(request, activity.window, webView,
|
|
31
|
+
frames = { ids, revision -> renderer.measure(ids, revision) },
|
|
32
|
+
views = { ids -> renderer.viewsAllowedToHide(ids) },
|
|
33
|
+
complete = { result ->
|
|
34
|
+
result.fold(onSuccess = reply, onFailure = reject)
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`renderer` and `reply` above are the plugin's existing layout code and bridge
|
|
39
|
+
completion callback. On Android, forward a failed `Result` to the bridge's
|
|
40
|
+
rejection callback (`reject` above); Web uses that failure to abandon the image
|
|
41
|
+
and reconcile presentation. Do not acknowledge a failed drawing boundary as
|
|
42
|
+
success. `frames` may run repeatedly while geometry settles; it must
|
|
43
|
+
recheck the requested revision and capture eligibility. It must not mutate layout.
|
|
44
|
+
If multiple controls share a native container, `views` must reject a partial set
|
|
45
|
+
that would hide uncaptured controls. The Engine cannot infer that relationship.
|
|
46
|
+
The renderer also keeps its existing native interaction notifications.
|
|
47
|
+
|
|
48
|
+
`cover` installs prepared pixels before hiding those views. Destination parts are
|
|
49
|
+
validated against the cached token and used for both cover and restore, so moving
|
|
50
|
+
a prepared surface does not reuse an old position. Invalid or unavailable handoffs
|
|
51
|
+
return `shown: false`. `painted` and `restored` retire only the corresponding copy;
|
|
52
|
+
Android waits for Web drawing readiness. `release` and `reset` retire owned
|
|
53
|
+
resources. No capture starts in the cover or restore path.
|
|
54
|
+
|
|
55
|
+
The renderer still performs its ordinary native update between `restore` and the
|
|
56
|
+
accepted update acknowledgement. Before that update, call `restoreControls()` on
|
|
57
|
+
iOS or `restoreViews()` on Android to undo only the Engine's hiding changes. This
|
|
58
|
+
is not a command to lay out or reveal arbitrary plugin content. Keep the normal
|
|
59
|
+
visibility and placement policy in the renderer.
|
|
60
|
+
|
|
61
|
+
### Direct snapshot primitives
|
|
62
|
+
|
|
63
|
+
Use these only when your integration owns native placement during handoff. `NativeUIProjection` uses the same operations with registration-specific placement and opaque-view policies. Do not duplicate conversion or copy retirement when forwarding requests to `handle`.
|
|
64
|
+
|
|
65
|
+
- Convert destination `parts` from CSS pixels using `webView.bounds.width / viewportWidth` on iOS or `webView.width / viewportWidth` on Android. Pass frames to `cover` and `restore`; absent cover parts retain captured placement. Neither operation captures new pixels.
|
|
66
|
+
- `transient: true` separates a moving foreground surface from the background's `coveringToken`. Reuse pixels while source sizes and positions relative to their scope remain unchanged; movement within a fixed scope invalidates the layout.
|
|
67
|
+
- Retire the entry copy after Web drawing readiness: `retireCopy(token)` on iOS, `retireCopyAfterWebDraw` on Android.
|
|
68
|
+
- For return, call `restore`, undo Engine hiding with `restoreControls()` / `restoreViews()`, and apply normal native layout. Retire the return copy after the accepted native update and Web drawing readiness, using `retireCopy` with `entry: false` / `entry = false`. Android can forward `painted` and `restored` to `handle` to reuse readiness and rejection handling.
|
|
69
|
+
- Release tokens with `release(token)`. Use `reset()` only for owner teardown, and Android `close()` to dispose the encoder. Never reset another surface's captures.
|
|
70
|
+
|
|
71
|
+
Missing captures or placement use the renderer's ordinary fallback. See the [Web request contract](web-projection.md#multiple-fallbacks-in-one-native-transaction) for payloads.
|
|
72
|
+
|
|
73
|
+
Use `NativeUICoveredSnapshots.observe` to notify the JS listener when interaction starts/ends. On iOS, captures rasterize the visible window with `drawHierarchy(in:afterScreenUpdates:)`, including the background under the clipped native component. Screen snapshot views are display replicas and are not used as bitmap sources. UIKit rasterization can differ from the live compositor for materials such as Liquid Glass; validate the appearance on physical devices with the backgrounds your plugin supports. Cache validity therefore includes layout and the plugin's visual-change notifications. The engine does not capture continuously or guarantee every possible renderer can be captured.
|
|
74
|
+
|
|
75
|
+
## Native scroll tracking
|
|
76
|
+
|
|
77
|
+
A native control inside a Web scrolling list would otherwise need new placement
|
|
78
|
+
messages as the list moves. Use these bindings only when the renderer already
|
|
79
|
+
owns scroll reconciliation: a uniquely matched iOS overflow scroll view can
|
|
80
|
+
carry native content with it. Nested or ambiguous scrolling must use the ordinary
|
|
81
|
+
fallback instead of attaching content to the wrong scroll owner.
|
|
82
|
+
|
|
83
|
+
`readScrollPlacement(element, owner, identify)` measures one untransformed overflow owner. `createScrollBindings()` validates reuse and detects movement while acquisition is awaiting ACK. Keep `reuse` out of layout-deduplication signatures; it describes acknowledgement state, not layout.
|
|
84
|
+
|
|
85
|
+
On iOS, `NativeUIScrollBindings.replace` groups sources by owner and binds only an unambiguous matching `UIScrollView`. `update()` applies positions and clipping without rebuilding content. `NativeUIClippedView` preserves full content dimensions while restricting visible pixels and hit testing.
|
|
86
|
+
|
|
87
|
+
An established binding rejected by native loses ownership and may be freshly acquired through the **same** update/ACK path. A failed fresh acquisition stays on the fallback until a relevant layout/scroll-end event. Never add an immediate retry loop or replay user actions. A frame/props change does not recalibrate an established coordinate origin.
|
|
88
|
+
|
|
89
|
+
This uses WKWebView's internal overflow hierarchy without private class names. Missing/ambiguous views, nested scrolling, document scrolling, unsupported transforms, shaped clipping, or a moving unconfirmed owner cannot be bound. Do not claim support solely because a `UIScrollView` exists. A native view may intercept gestures that begin inside it; routing those gestures and SDK accessibility behavior remain renderer-specific.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Advanced: coordinate pages and modals in an existing renderer
|
|
2
|
+
|
|
3
|
+
Here, a **renderer** means the part of your plugin that creates native views and
|
|
4
|
+
updates their layout and appearance. Use this guide when it manages multiple native
|
|
5
|
+
controls, batches their updates, and knows which page or modal owns them. The
|
|
6
|
+
Engine takes over image preparation, scope reuse and handoff cleanup so those
|
|
7
|
+
concerns do not need separate implementations for every overlay.
|
|
8
|
+
|
|
9
|
+
For a single native view, start with [Capacitor](../getting-started/capacitor.md) or
|
|
10
|
+
[Cordova](../getting-started/cordova.md) registration instead. For the shared model and limits,
|
|
11
|
+
read [concepts](../concepts.md). The APIs below are for an existing renderer, not
|
|
12
|
+
additional steps required by those plugin guides. See [native initialization](../getting-started/initialization.md)
|
|
13
|
+
for the basic API and its optional settings before choosing this route.
|
|
14
|
+
|
|
15
|
+
## Group controls by the lifecycle they share
|
|
16
|
+
|
|
17
|
+
A **surface** groups controls that must hand off together, such as the buttons on
|
|
18
|
+
a page or modal. It is not necessarily one native view or image.
|
|
19
|
+
`createWebProjectionSurface` manages their prepared images and fallback scopes;
|
|
20
|
+
`createWebProjectionSurfaces` manages the lifetime of multiple such surfaces.
|
|
21
|
+
|
|
22
|
+
Separate groups matter because covering a background page does not necessarily
|
|
23
|
+
cover the foreground modal's controls. Sharing one lifecycle would make those
|
|
24
|
+
independent presentation decisions interfere with each other.
|
|
25
|
+
|
|
26
|
+
For example, a plugin rendering toolbar buttons and a tab bar can use separate
|
|
27
|
+
surfaces for a background page and each modal:
|
|
28
|
+
|
|
29
|
+
| Surface | Native content | When a Web image is useful |
|
|
30
|
+
| --------------- | --------------------------- | -------------------------------------------------------------------------- |
|
|
31
|
+
| Background page | Toolbar buttons and tab bar | A Web modal or popover must cover the page's controls. |
|
|
32
|
+
| Modal | Close and confirm buttons | The modal moves during a drag, or another Web overlay covers its controls. |
|
|
33
|
+
|
|
34
|
+
When a modal opens, the background surface can switch to its prepared images while
|
|
35
|
+
the modal's own controls remain Native when appropriate. Closing the modal requests
|
|
36
|
+
restoration of the background controls at their current positions; their images are
|
|
37
|
+
removed after the accepted Native update. Separate surfaces prevent a departing
|
|
38
|
+
modal's images or late replies from taking ownership of the newly exposed page.
|
|
39
|
+
|
|
40
|
+
## Connect the existing renderer
|
|
41
|
+
|
|
42
|
+
Connect each phase to the corresponding event in your renderer. These snippets
|
|
43
|
+
show separate lifecycle points, not calls to execute together during initialization.
|
|
44
|
+
The renderer supplies `transport`, source IDs and frames, accepted revisions and
|
|
45
|
+
its existing update scheduler. In this example, `ownerOf(element)` is the
|
|
46
|
+
renderer's existing lookup of the page or modal that owns a control.
|
|
47
|
+
|
|
48
|
+
### Create a surface when its owner becomes available
|
|
49
|
+
|
|
50
|
+
Map native control sources to existing fallback containers. The collection reuses
|
|
51
|
+
the surface for repeated updates to the same owner. The owner passed to `ensure`
|
|
52
|
+
is a lifecycle key, not a source filter: `surfaces.consider` broadcasts the batch
|
|
53
|
+
to every surface. Each surface uses `eligible` to select only its own controls;
|
|
54
|
+
two surfaces must not manage the same fallback container:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { createWebProjectionSurface, createWebProjectionSurfaces, type WebProjectionSurface } from '@webview-native-ui/engine';
|
|
58
|
+
|
|
59
|
+
const surfaces = createWebProjectionSurfaces<HTMLElement, WebProjectionSurface>();
|
|
60
|
+
const projection = await surfaces.ensure(page, () =>
|
|
61
|
+
createWebProjectionSurface(transport, {
|
|
62
|
+
namespace: 'my-renderer',
|
|
63
|
+
target: (source) => source.element,
|
|
64
|
+
eligible: (source) => ownerOf(source.element) === page,
|
|
65
|
+
wake: scheduleUpdate,
|
|
66
|
+
blocked: () => transitionIsActive,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Prepare during normal updates, hand off at presentation boundaries
|
|
72
|
+
|
|
73
|
+
An accepted native update provides the appearance to prepare. Beginning a handoff
|
|
74
|
+
uses that prepared image; ending one requests restoration rather than immediately
|
|
75
|
+
removing it. While a surface owns the image, exclude its `owners` from ordinary
|
|
76
|
+
native rendering so live controls do not appear above the covering Web layer.
|
|
77
|
+
Use `visibleOwners` for return-image ownership while restoring. Retire the image
|
|
78
|
+
only after the normal native update is acknowledged.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// After accepting an ordinary native update:
|
|
82
|
+
surfaces.consider(acceptedRevision, sources);
|
|
83
|
+
|
|
84
|
+
// When the framework reports coverage or motion for this owner:
|
|
85
|
+
projection?.begin();
|
|
86
|
+
|
|
87
|
+
// When that coverage or motion ends:
|
|
88
|
+
projection?.end();
|
|
89
|
+
// Send the renderer's normal native update, then on its acknowledgement:
|
|
90
|
+
surfaces.updated(token, restoredIDs, acceptedRevision);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Retire a page or the renderer
|
|
94
|
+
|
|
95
|
+
Deleting an owner prevents late creation or restoration from reviving its controls.
|
|
96
|
+
Destroy the collection when the renderer itself is disposed.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
await surfaces.delete(page); // Page retirement.
|
|
100
|
+
// At renderer teardown:
|
|
101
|
+
await surfaces.destroy();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Placement and shared monitoring
|
|
105
|
+
|
|
106
|
+
The renderer supplies Native IDs and frames, source ownership, placement policy and
|
|
107
|
+
accepted update acknowledgements. Framework lifecycle events remain outside the
|
|
108
|
+
Engine; it does not infer modal semantics from arbitrary DOM mutations.
|
|
109
|
+
|
|
110
|
+
For renderer-owned fallback proxies, `canRelocate(target)` explicitly permits
|
|
111
|
+
`begin(layer)` to place those hosts in a supplied overlay layer. The Engine restores
|
|
112
|
+
their parent and sibling order after restoration, failure or disposal. Application
|
|
113
|
+
DOM is not relocated by default, and an application reparenting a proxy takes
|
|
114
|
+
precedence over restoring its old location.
|
|
115
|
+
|
|
116
|
+
A collection shares appearance, resize, orientation and document visibility
|
|
117
|
+
listeners across its surfaces, and ignores Engine-owned image events. Renderers can
|
|
118
|
+
exclude their own proxy events with `ignoreTarget`. `followMotion(moving, target)`
|
|
119
|
+
uses the renderer's existing moving-element map to rebase images only while their
|
|
120
|
+
own hosts animate; ancestor transforms already move the images without polling.
|
|
121
|
+
All listeners and scheduled frames are removed at disposal.
|
|
122
|
+
|
|
123
|
+
Repeated `ensure` calls share pending creation or the existing projection. Deletion,
|
|
124
|
+
DOM disconnection or renderer disposal prevents a late result from becoming active;
|
|
125
|
+
the Engine destroys it instead. A failed creation may be retried. The renderer
|
|
126
|
+
signals page retirement through its existing lifecycle and can inspect `keys()`
|
|
127
|
+
and `values()` during its normal update pass. The collection does not install a
|
|
128
|
+
second DOM mutation observer or a continuous polling loop.
|
|
129
|
+
|
|
130
|
+
## Verify the handoff in the consuming renderer
|
|
131
|
+
|
|
132
|
+
Exercise an opening overlay, dismissal, a moving modal and owner removal during
|
|
133
|
+
restoration. The background and foreground must retain their own controls; a late
|
|
134
|
+
acknowledgement must not restore a departed owner. Change position without changing
|
|
135
|
+
appearance to check image reuse, then change content or dimensions to check
|
|
136
|
+
invalidation. Also exercise missing images: the ordinary fallback must remain
|
|
137
|
+
available without delaying overlay startup.
|
|
138
|
+
|
|
139
|
+
The Engine coordinates ownership, but only the consuming application can verify
|
|
140
|
+
its layer choices and native placement. Implement the native side using the
|
|
141
|
+
[native handoff contract](native-handoff.md), using `NativeUICoveredSnapshots.handle` on iOS and `NativeUISnapshots.handle` on
|
|
142
|
+
Android. Supply eligible native bounds and complete views allowed to hide; Engine
|
|
143
|
+
validates requests and converts destination coordinates. Your renderer still
|
|
144
|
+
applies normal native layout and reports accepted update acknowledgements.
|
|
145
|
+
|
|
146
|
+
For different ownership needs, see [low-level Web projection](web-projection.md).
|
|
147
|
+
Update scheduling is independent: use the optional [update queue](update-queue.md)
|
|
148
|
+
only when it fits the renderer's reconciliation policy.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Serialize state updates and recover a lost acknowledgement
|
|
2
|
+
|
|
3
|
+
Use `createNativeUpdateQueue` when a renderer reads DOM state asynchronously and
|
|
4
|
+
sends native updates. Event bursts should produce one current-state read, and a
|
|
5
|
+
second read must not overlap the previous pass. This helper is optional: it does
|
|
6
|
+
not change native registrations or other projection APIs. Keep an existing scheduler
|
|
7
|
+
if the renderer needs a different cadence or recovery policy. The queue batches requests on
|
|
8
|
+
`requestAnimationFrame`; requests arriving during a pass trigger one later pass.
|
|
9
|
+
|
|
10
|
+
For example, a resize, a selection update and a DOM mutation arrive in one frame
|
|
11
|
+
while the previous native update is still awaiting acknowledgement. Queue one
|
|
12
|
+
fresh-state pass instead of starting overlapping reads. If the bridge loses an
|
|
13
|
+
acknowledgement, reconcile current state before restoring updates; never use this
|
|
14
|
+
queue to retry a purchase, click or advertisement load.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createNativeUpdateQueue } from '@webview-native-ui/engine';
|
|
18
|
+
|
|
19
|
+
const updates = createNativeUpdateQueue(window, {
|
|
20
|
+
async update(send) {
|
|
21
|
+
const state = await renderer.readCurrentState();
|
|
22
|
+
const reply = await send(
|
|
23
|
+
() => renderer.sendState(state),
|
|
24
|
+
(reply) => {
|
|
25
|
+
if (reply.revision !== state.revision) throw new Error('Revision mismatch');
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
if (reply) renderer.accept(state, reply);
|
|
29
|
+
},
|
|
30
|
+
retry: () => renderer.invalidateLastAcceptedState(),
|
|
31
|
+
failed: (error) => renderer.stopAndRestoreFallback(error),
|
|
32
|
+
reconnect: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// From existing layout/content notifications:
|
|
36
|
+
updates.request();
|
|
37
|
+
// After the existing connection check has reconciled native session/revision:
|
|
38
|
+
updates.reconnected();
|
|
39
|
+
// At renderer teardown:
|
|
40
|
+
updates.destroy();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`renderer` represents the integration's existing code. `send` is only for
|
|
44
|
+
replaceable state, called sequentially inside an update pass, never clicks,
|
|
45
|
+
selection actions, purchases or SDK requests. A failed send skips the remaining sends in that pass and requests one fresh
|
|
46
|
+
pass, not a replay of its payload. If that also fails, `reconnect: true` waits for
|
|
47
|
+
an external handshake; without it the queue reports terminal failure. An invalid
|
|
48
|
+
acknowledgement waits for reconciliation immediately. After reconciliation, another
|
|
49
|
+
failed retry or invalid acknowledgement is terminal; a fully successful update pass resets the
|
|
50
|
+
recovery budget. The queue does not install a connection poller or subscriptions.
|
|
51
|
+
|
|
52
|
+
Keep operation timeouts in the transport. A timeout does not cancel an underlying
|
|
53
|
+
native call: the native renderer must reject stale revisions, and the connection
|
|
54
|
+
check must reconcile current state before declaring recovery. Disposal suppresses
|
|
55
|
+
late send results and future passes; it cannot undo a native operation already sent.
|
|
56
|
+
Use `dirty` to reject obsolete DOM measurements, `running` to defer connection
|
|
57
|
+
reconciliation during a pass, and `waiting` to detect a pending recovery. The
|
|
58
|
+
renderer still owns revision assignment, accepted geometry and fallback policy.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Low-level Web projection
|
|
2
|
+
|
|
3
|
+
Use the primitives below only if the surface helpers do not fit your renderer's
|
|
4
|
+
existing ownership model. `createWebProjectionSurface` already manages scopes and
|
|
5
|
+
batch handoffs; manually creating another manager for the same fallback would
|
|
6
|
+
introduce competing ownership.
|
|
7
|
+
|
|
8
|
+
For example, a renderer already owns one fallback container per native panel and
|
|
9
|
+
accepts native updates through its own revision protocol. A projection scope lets
|
|
10
|
+
it retain those ownership decisions while delegating image preparation and return
|
|
11
|
+
coordination. Prefer the surface helpers if page or modal grouping already fits;
|
|
12
|
+
a second manager on the same container would compete for its pixels.
|
|
13
|
+
|
|
14
|
+
A scope binds one existing Web fallback element for its lifetime. Obtain that element from your plugin's DOM; the engine does not create a replacement host or choose a page, stacking order or z-index. A different fallback element requires a new scope. Its Document is derived from `element.ownerDocument`.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createWebProjectionScope, createWebProjection } from '@webview-native-ui/engine';
|
|
18
|
+
|
|
19
|
+
const fallback = container.querySelector<HTMLElement>('[data-web-fallback]');
|
|
20
|
+
if (!fallback) throw new Error('Web fallback is missing');
|
|
21
|
+
const scope = createWebProjectionScope(fallback, 'my-plugin');
|
|
22
|
+
const projection = await createWebProjection(
|
|
23
|
+
scope,
|
|
24
|
+
{
|
|
25
|
+
send: (request) => plugin.snapshot(request),
|
|
26
|
+
listen: (listener) => plugin.addListener('snapshotInteraction', listener),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
wake: () => schedulePluginUpdate(),
|
|
30
|
+
blocked: () => overlayIsActive(),
|
|
31
|
+
revision: () => lastSentRevision,
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The container, bridge and update callbacks above belong to the consuming plugin. No `document`, `target`, `layer`, CSS selector or layer resolver is passed to the projection manager.
|
|
37
|
+
|
|
38
|
+
1. After an accepted native update, call `projection.consider(revision, sources)`. Sources associate the native IDs and viewport frames with their source DOM; they need not be inside the fallback element. Images are captured and decoded in advance.
|
|
39
|
+
2. Call `projection.changed()` for appearance changes, or `projection.changed(scrollElement)` to invalidate scroll geometry. Layout updates go through `consider`; use `invalidate()` for other geometry changes.
|
|
40
|
+
3. Before covering Native with a Web overlay, call `projection.begin()`. It displays already prepared pixels at the fixed fallback. No new capture or decode starts here. If capture is unavailable, the plugin keeps its ordinary Web fallback.
|
|
41
|
+
4. Exclude `projection.owners` from normal Native updates while the image owns those sources. `visibleOwners` keeps their return pixels while Native is being restored; `preparedOwners` identifies sources with prepared images. These arrays are read-only snapshots.
|
|
42
|
+
5. Call `projection.end()`, send the normal native update, then call `projection.updated(snapshotToken, acceptedIDs, revision)`. A newer acknowledgement containing all captured IDs retires the image.
|
|
43
|
+
6. Call `projection.destroy()` when disposing the surface. It releases only its own images and listener; the plugin owns final Native reset and its DOM.
|
|
44
|
+
|
|
45
|
+
The engine prepares a noninteractive `img` inside the fallback container (directly inside its open ShadowRoot when present) while Native is visible. It reuses the decoded image and DOM node across handoffs, and releases them when the scope is retired or destroyed. The image is `inert`, hidden from accessibility and excluded from hit testing. The existing fallback and its children are never cloned, replaced, removed or assigned `inert`/`aria-hidden`; their listeners, values and application changes survive projection. Both `::before` and `::after` remain available to the application.
|
|
46
|
+
|
|
47
|
+
Supply an existing container such as `div`, `button` or a custom element. Replaced/void elements such as `img`, `input` and `video` remain unsupported as scopes; use their existing fallback container. The renderer may move the same container between parents, but must not replace a low-level scope's element or let two managers control it simultaneously. Notify the manager through `moved()` when changing its placement during projection.
|
|
48
|
+
|
|
49
|
+
The image is absolutely positioned within the fallback, which retains its layout box. Static hosts become positioned while the image is visible. Host and ancestor clipping, transforms and filters still apply. Open ShadowRoot insertion bypasses internal slot wrappers, not the host's own clipping. Closed shadow roots cannot be inspected; provide a fallback container that can render the image.
|
|
50
|
+
|
|
51
|
+
Engine-owned nodes carry `data-native-ui-projection-node` for their entire pooled lifetime. Renderers that inspect fallback children must exclude these nodes from application content discovery. A renderer that uses a MutationObserver to invalidate captures must also ignore insertion/removal and attribute changes of these nodes, changes inside them, and Engine-owned `data-native-ui-snapshot-*` attributes. Continue observing authored content, dimensions and disabled-state changes; ignoring all mutations on a projection host would hide real updates. Otherwise image preparation can invalidate itself and cause repeated capture/release cycles. The engine removes only its own nodes on disposal. Child insertion can affect structural selectors within the fallback, but does not add items alongside it in flex, grid or virtual lists.
|
|
52
|
+
|
|
53
|
+
### Notifications from the existing plugin
|
|
54
|
+
|
|
55
|
+
The low-level manager can subscribe to the existing plugin's events through `options.events`:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
events: {
|
|
59
|
+
addListener: (name, listener) => plugin.addListener(name, listener),
|
|
60
|
+
update: ['setColors', 'setTabs', 'tabSelected'],
|
|
61
|
+
projection: {
|
|
62
|
+
native: ['shown'],
|
|
63
|
+
web: ['transitionStarted'],
|
|
64
|
+
none: ['hidden'],
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
These are **notifications**, not intercepted method names. The plugin emits update events after its native changes succeed. A Web projection event must arrive before movement starts; it uses the previously prepared image. A native event requests restoration, but the image remains until the normal renderer acknowledgement confirms that Native is ready. A hidden event follows the plugin's native hide operation and retires the image; it does not invoke `hide()` or change application-owned fallback DOM. Appearance updates preserve the requested destination, including `none`.
|
|
70
|
+
|
|
71
|
+
For plugins with several surfaces, provide `accepts(event)` to select this scope's stable surface identity. Events shared by `update` and a destination are subscribed once and processed together. Conflicting destinations for the same event are rejected before subscription. Destroying the manager removes all its subscriptions, including registrations that resolve late after initialization fails. No public plugin methods are wrapped or replayed.
|
|
72
|
+
|
|
73
|
+
The manager uses the host runtime's resource URL conversion internally (Capacitor or Cordova). Plugin authors do not supply a URL resolver.
|
|
74
|
+
|
|
75
|
+
This section describes the lower-level manager for renderers that already own batch reconciliation, rather than a single native-view registration. Prefer the native registration in the plugin guides for a new integration; it owns startup, measurements and draw acknowledgements.
|
|
76
|
+
|
|
77
|
+
### Multiple fallbacks in one Native transaction
|
|
78
|
+
|
|
79
|
+
For example, a toolbar contains two button groups with separate fallback hosts,
|
|
80
|
+
but the renderer hides and restores them in one native transaction. Independent
|
|
81
|
+
managers could expose one group before the other. A batch uses one handoff token
|
|
82
|
+
for the groups so they can cross the rendering boundary together.
|
|
83
|
+
|
|
84
|
+
Use `createWebProjectionBatch(transport, options)` when several fallback elements must switch together. It uses the same image engine, one listener and one capture/cover/restore token. Pass `{ scope, element, control, ... }` for each entry to `batch.consider(revision, sources)`. Several native IDs may share one scope, such as a button group.
|
|
85
|
+
|
|
86
|
+
The batch derives its Document from the first nonempty `consider`; all subsequent scopes and sources must belong to that Document. Before this first update, `begin()` does nothing and `destroy()` is safe. A changed fallback requires a new scope. Scope identity participates in cache invalidation; an active presentation retains its captured scopes until restoration finishes.
|
|
87
|
+
|
|
88
|
+
`WebProjectionRequest` is an action-discriminated union. `prepare` requires `revision`, `viewportWidth` and `ids`; `cover` requires `token` and includes `viewportWidth` and destination `parts` for transient movement; `restore` requires `token`, `viewportWidth` and destination `parts`; `painted`, `restored` and `release` require `token`; `reset` takes no payload. Empty preparation results and `{ shown: false }` mean the plugin should use its normal Web fallback. Use the plugin's existing ordered bridge; the engine does not retry user actions.
|