@webview-native-ui/engine 0.0.6 → 0.0.7

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.
Files changed (45) hide show
  1. package/README.md +19 -12
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +3 -0
  4. package/dist/native/session.d.ts +5 -0
  5. package/dist/native/session.js +83 -14
  6. package/dist/native/updates.d.ts +16 -0
  7. package/dist/native/updates.js +108 -0
  8. package/dist/native/values.d.ts +5 -0
  9. package/dist/native/values.js +27 -0
  10. package/dist/projection/surface.d.ts +35 -0
  11. package/dist/projection/surface.js +81 -0
  12. package/dist/projection/surfaces.d.ts +20 -0
  13. package/dist/projection/surfaces.js +116 -0
  14. package/docs/README.md +70 -0
  15. package/docs/advanced/README.md +20 -0
  16. package/docs/advanced/android-placement.md +48 -0
  17. package/docs/advanced/native-handoff.md +89 -0
  18. package/docs/advanced/renderer.md +148 -0
  19. package/docs/advanced/update-queue.md +58 -0
  20. package/docs/advanced/web-projection.md +88 -0
  21. package/docs/assets/ios-native-capture-comparison.png +0 -0
  22. package/docs/concepts.md +97 -0
  23. package/docs/configuration/README.md +29 -0
  24. package/docs/configuration/capture.md +27 -0
  25. package/docs/configuration/events.md +35 -0
  26. package/docs/configuration/ionic-overlays.md +65 -0
  27. package/docs/configuration/values.md +125 -0
  28. package/docs/cordova.md +1 -1
  29. package/docs/{capacitor-plugin.md → getting-started/capacitor.md} +45 -47
  30. package/docs/{cordova-plugin.md → getting-started/cordova.md} +32 -15
  31. package/docs/getting-started/initialization.md +79 -0
  32. package/docs/maintenance.md +14 -2
  33. package/docs/{projection-containers.md → reference/containers.md} +6 -2
  34. package/docs/reference/platforms.md +137 -0
  35. package/docs/reference/projection.md +134 -0
  36. package/docs/reference/state.md +39 -0
  37. package/ios/Sources/WebViewNativeUIEngine/ClippedView.swift +12 -12
  38. package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +68 -7
  39. package/ios/Sources/WebViewNativeUIEngine/Frame.swift +10 -10
  40. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +74 -39
  41. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -3
  42. package/ios/Sources/WebViewNativeUIEngine/ScrollBindings.swift +2 -0
  43. package/ios/Sources/WebViewNativeUIEngine/ScrollTracker.swift +9 -7
  44. package/package.json +9 -2
  45. package/docs/projection.md +0 -368
@@ -20,7 +20,7 @@ public struct NativeUIScrollPlacement: Decodable, Equatable {
20
20
 
21
21
  public var isValid: Bool {
22
22
  !id.isEmpty && viewport.isValid && clip.isValid && frame.isValid &&
23
- offsetX.isFinite && offsetY.isFinite && contentWidth.isFinite && contentHeight.isFinite && contentWidth > 0 && contentHeight > 0
23
+ offsetX.isFinite && offsetY.isFinite && contentWidth.isFinite && contentHeight.isFinite && contentWidth > 0 && contentHeight > 0
24
24
  }
25
25
  }
26
26
 
@@ -38,11 +38,11 @@ public final class NativeUIScrollTracker {
38
38
  private var observation: NSKeyValueObservation?
39
39
 
40
40
  public init?(webView: WKWebView, nodes: [NativeUIScrollNode], scale: CGFloat,
41
- render: @escaping (String, CGRect, CGRect) -> Void) {
41
+ render: @escaping (String, CGRect, CGRect) -> Void) {
42
42
  guard let placement = nodes.first?.scroll, placement.isValid,
43
43
  nodes.allSatisfy({ $0.scroll?.id == placement.id && $0.scroll?.viewport == placement.viewport &&
44
- $0.scroll?.clip == placement.clip && $0.scroll?.contentWidth == placement.contentWidth &&
45
- $0.scroll?.contentHeight == placement.contentHeight }), scale.isFinite, scale > 0 else { return nil }
44
+ $0.scroll?.clip == placement.clip && $0.scroll?.contentWidth == placement.contentWidth &&
45
+ $0.scroll?.contentHeight == placement.contentHeight }), scale.isFinite, scale > 0 else { return nil }
46
46
  let transform = CGAffineTransform(scaleX: scale, y: scale)
47
47
  let viewport = placement.viewport.rect.applying(transform)
48
48
  let candidates = Self.scrollViews(in: webView).filter { scroll in
@@ -76,9 +76,9 @@ public final class NativeUIScrollTracker {
76
76
  guard self.webView === webView, let scrollView, scrollView.isDescendant(of: webView),
77
77
  !scrollView.isHidden, scrollView.window != nil, self.scale == scale,
78
78
  nodes.allSatisfy({ node in
79
- guard let next = node.scroll else { return false }
80
- return next.id == placement.id && next.viewport == placement.viewport && next.clip == placement.clip &&
81
- next.contentWidth == placement.contentWidth && next.contentHeight == placement.contentHeight
79
+ guard let next = node.scroll else { return false }
80
+ return next.id == placement.id && next.viewport == placement.viewport && next.clip == placement.clip &&
81
+ next.contentWidth == placement.contentWidth && next.contentHeight == placement.contentHeight
82
82
  }) else { return false }
83
83
  setFrames(nodes)
84
84
  return true
@@ -86,6 +86,8 @@ public final class NativeUIScrollTracker {
86
86
 
87
87
  private func setFrames(_ nodes: [NativeUIScrollNode]) {
88
88
  frames = Dictionary(uniqueKeysWithValues: nodes.map {
89
+ // init and replace validate every scroll placement before reaching here.
90
+ // swiftlint:disable:next force_unwrapping
89
91
  ($0.id, $0.scroll!.frame.rect.applying(CGAffineTransform(scaleX: scale, y: scale))
90
92
  .offsetBy(dx: contentOrigin.x, dy: contentOrigin.y))
91
93
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webview-native-ui/engine",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Native UI placement, scroll tracking, and Web projection for Capacitor and Cordova plugin maintainers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,16 +37,23 @@
37
37
  "test:package": "node scripts/verify-package.mjs",
38
38
  "release": "np --no-publish --no-tests --branch main",
39
39
  "test:cordova": "node scripts/verify-cordova.mjs",
40
- "format:check": "prettier --check \"**/*.{ts,js,mjs,cjs,json,md,yml,yaml,html}\""
40
+ "format:check": "prettier --check \"**/*.{ts,js,mjs,cjs,json,md,yml,yaml,html}\"",
41
+ "swiftlint": "node-swiftlint",
42
+ "lint:swift": "swiftlint version && node-swiftlint lint --strict --no-cache",
43
+ "format:swift": "swiftlint version && node-swiftlint --fix --format --no-cache",
44
+ "lint:android": "android/gradlew -p android ktfmtCheck lintRelease",
45
+ "format:kotlin": "android/gradlew -p android ktfmtFormat"
41
46
  },
42
47
  "devDependencies": {
43
48
  "@eslint/js": "^10.0.1",
49
+ "@ionic/swiftlint-config": "^2.0.0",
44
50
  "@playwright/test": "^1.58.0",
45
51
  "esbuild": "^0.27.0",
46
52
  "eslint": "^10.13.0",
47
53
  "globals": "^17.13.0",
48
54
  "np": "^10.2.0",
49
55
  "prettier": "^3.6.2",
56
+ "swiftlint": "^2.0.0",
50
57
  "typescript": "^5.9.3",
51
58
  "typescript-eslint": "^8.72.1"
52
59
  },
@@ -1,368 +0,0 @@
1
- # Projection behavior and advanced integration
2
-
3
- Start with the [Capacitor](capacitor-plugin.md) or [Cordova](cordova-plugin.md) guide. This reference explains shared DOM requirements, destination rules and the lower-level APIs for existing renderers.
4
-
5
- ## Ownership
6
-
7
- | Engine | Consuming plugin |
8
- | ------------------------------------------------------------------- | -------------------------------------------------------------------- |
9
- | DOM measurement, image geometry and supported native scroll binding | Creating, attaching, positioning and sizing native views |
10
- | Startup, event subscriptions and native draw acknowledgements | Providing the fallback DOM at the correct layer |
11
- | Prepared image cache, decoding, display and retirement | Reporting appearance changes and the intended projection destination |
12
- | Native capture and temporary return images | Providing native views and SDK capture eligibility |
13
- | Image invalidation, token cleanup and observer teardown | Owning business actions and the interactive Web fallback |
14
-
15
- A DOM slot does not necessarily contain a Web replacement: a UI plugin can restore a button, while an ad plugin may hide its slot. Image handoff is explicit and noninteractive. The engine never forwards clicks through an image or replaces SDK click/impression handling.
16
-
17
- ## Projection contract
18
-
19
- A notification specifies **where the surface belongs**, not how to capture it. The Engine chooses the platform-specific handoff without requiring the plugin to keep another copy of projection state.
20
-
21
- | Intended destination | Required behavior |
22
- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
23
- | `native` | Show the interactive native view at confirmed geometry. During motion, use supported native tracking or a prepared image. |
24
- | `web` | Keep native content below the covering Web layer by removing its native projection; use a prepared image or the plugin's Web fallback. |
25
- | `none` | Retire native content, projected images and fallback content. Later update/capture callbacks must not show it again. |
26
-
27
- Overlay coverage can temporarily turn `native` into `web`; it never overrides `none`. A capture failure does not change the intended destination. `failedFallback: native` is only valid for consumers that maintain native placement themselves, and never overrides explicit `web`, `none`, or overlay coverage.
28
-
29
- The Engine owns image readiness and restoration acknowledgement. It must not hide native pixels before replacement pixels are ready, nor retire replacement pixels before native restoration succeeds. Position-only motion reuses prepared content; appearance, size and renderer identity changes invalidate it. A missing image uses the declared fallback immediately rather than waiting to capture during a transition.
30
-
31
- Registration and notification forwarding retain the latest intent across startup and reconnection; they do not replay business events. Destruction retires the registration and its cached images. Notifications are intent, not completion promises: an API that promises completed hiding must await the explicit retirement acknowledgement described under DOM and native surface requirements.
32
-
33
- An existing renderer that checks CSS visibility can call `getUnprojectedVisibility(element)` from the npm package. It reads current authored/inherited visibility without the Engine's hiding rules, and restores projection ownership before returning. Do not use Engine marker attributes to infer projection ownership or mutate them in a consuming plugin. Excluding Engine-owned DOM mutations from renderer invalidation is described below. This helper performs a style read and can trigger attribute observers; it does not eliminate measurement or transport costs.
34
-
35
- ## DOM and native surface requirements
36
-
37
- `projectionSelector` is required and must resolve to **one existing container when projection starts**, using a valid CSS selector. ID, class, attribute and descendant selectors are supported; zero or multiple matches leave Native hidden. The engine does not create the fallback container or choose its size, placement, layer or z-index. Keep the container's normal layout and any interactive Web controls; an empty zero-size placeholder is not a usable fallback. It is a fixed scope, not a discovery root.
38
-
39
- The container does not have to exist at registration time. A missing DOM waits without showing native content; replacing it creates a new binding and retires old images. The engine preserves the container's children, listeners and values. Pass the native view itself through `view`; a group of controls can use their common native container. The engine manages the internal identifier, so no ID list or view dictionary is required. Independent surfaces use separate registrations.
40
-
41
- A Web Component host can be the destination, for example `projectionSelector: 'native-ad[placement="home"]'`. Give the host a layout box (`display: block` with the intended dimensions); the engine observes insertion, removal and selector attribute changes. The selector runs in the document and does not search inside shadow roots. The engine suppresses fallback content and pseudo-elements in the light DOM and open shadow roots during projection. Closed shadow content cannot be inspected and must inherit the host's `visibility`; provide a container whose shadow/slot structure can display the inserted image.
42
-
43
- See [Projection container support](projection-containers.md) for the HTML and complete Ionic element tables, sizing conditions and a wrapper example.
44
-
45
- ### Native surface requirements
46
-
47
- For independently moving AdMob slots, retain one registration per loaded native view/surface, use a slot-specific selector and filter the plugin's notifications through `receiveListeners.match` (`match: ["slotKey": slotKey, "loadId": loadId]` / `match = mapOf("slotKey" to slotKey, "loadId" to loadId)`). Dispose the registration when a loaded ad is replaced, even if its host view is reused. Keep SDK loading, impression and click handling in the ad plugin. Supply `canCapture` with its existing capture eligibility checks (for example, test-ad / opaque-background / no-video restrictions); the engine does not grant permission to snapshot an SDK view.
48
-
49
- For SDK consumers without an equivalent Web fallback, set `failedFallback: .native`
50
- on iOS or `failedFallback = NativeUIProjection.FailedFallback.NATIVE` on Android.
51
- The default is `.web` / `WEB`, preserving existing consumers. If automatic motion
52
- cannot use a prepared image (not ready, unsupported capture, or rejected handoff),
53
- `native` preserves the live native view and keeps fallback DOM hidden. The consumer
54
- must continue positioning that view; this option does not create native scroll
55
- tracking on Android. Explicit `web`, `none`, and overlay coverage take precedence:
56
- this option never raises a native view above an intentionally covering Web layer.
57
-
58
- For opaque, software-renderable Android SDK views, use
59
- `captureMode = NativeUIProjection.CaptureMode.VIEW` together with an explicit
60
- `failedFallback` policy (`WEB` unless the consumer maintains native placement). This draws only the View's
61
- pixels with no surrounding padding, so clipping a partially visible ad cannot bake
62
- in a toolbar or adjacent content. Video, SurfaceView, translucent views, and content
63
- requiring Window composition must keep the default `COMPOSITED` mode. The consumer
64
- must provide a matching, clipped DOM host for the full opaque View; arbitrary native
65
- outline masks, transforms, and custom clipping are not reproduced.
66
-
67
- VIEW pixels are cached by native View identity, dimensions, and content revision.
68
- Ordinary scrolling retains the decoded Web image and updates placement without
69
- capture, PNG encoding, or image decode. The image covers the host border box at
70
- local origin (0, 0), independently of native placement acknowledgment latency.
71
- Opaque images remain painted beneath the live native View between scrolls.
72
- Before hiding that View, the engine waits for WebView visual readiness and a draw;
73
- VIEW creates no native entry or return image copies. Return acknowledgment waits
74
- across the native draw and the following animation frame. A generation check prevents
75
- a delayed cover callback from hiding content after restoration or suspension.
76
-
77
- The native Bitmap is recycled after PNG encoding in VIEW mode; the PNG and decoded
78
- Web image remain cached. Plugin update notifications advance the content revision:
79
- notify an update whenever content changes without replacing the View. Replacing the
80
- registration, WebView navigation, reset, or close retires the cache. Encoding stays
81
- off the UI thread; View drawing and cache ownership stay on it, with the existing
82
- 8-million-pixel cap.
83
-
84
- COMPOSITED retains native handoff copies for translucent content. Entry copies wait
85
- for WebView visual readiness and a draw before retirement. If the 500 ms entry
86
- deadline expires, the native View is restored before the copy is removed, and a late
87
- paint acknowledgment is rejected.
88
-
89
- Android VIEW consumers can read `isProjecting` on the UI thread after a successful
90
- cover to defer same-size native placement work. Keep only the latest accepted placement;
91
- continue validating sequence/generation, and process hide, resize and teardown immediately.
92
- Supply `beforeNativeReveal` to apply that pending placement synchronously before normal
93
- restoration, failure recovery, or active-image release reveals native pixels. The engine
94
- clears `isProjecting` before invoking the callback, so applying placement must not defer
95
- again. The callback may run without pending work and must be idempotent. Clear consumer
96
- pending work on hide/destroy and validate the loaded View identity before applying it.
97
- The callback does not replace content/size invalidation or JavaScript geometry tracking.
98
-
99
- On Android, consumers whose asynchronous hide method promises that all pixels have
100
- disappeared must call `projection.hide { completed -> ... }` and resolve only when
101
- `completed` is true. Native handoff copies disappear immediately; completion waits
102
- for synchronous Web-proxy retirement. A timeout reports failure, not success.
103
- Call `projection.show()` when presenting again, in addition to normal plugin events;
104
- this also preserves presentation intent if event subscription is still starting.
105
-
106
- Scroll observation follows composed-tree ancestors, including assigned slots.
107
- Non-composed scroll events inside an Ionic shadow tree are observed at their source;
108
- applications do not add an extra listener or dispatch synthetic document events.
109
- Listeners follow host reparenting and slot reassignment and are removed on teardown.
110
-
111
- Scrolling is internal: iOS uses a uniquely matched native scroll binding when available; otherwise, including Android, scrolling temporarily uses the prepared image and restores native content after motion settles. Automatic scroll handling never overrides explicit `web` or `none`. Unsupported capture or geometry keeps the normal DOM fallback; it does not wait for a new capture on the transition path. The latest explicit destination survives WebView reload, and old-document replies cannot complete a new document's requests.
112
-
113
- By default, the internal channel is limited to the main frame on Capacitor's localhost origins. Pass the application's exact `allowedOrigins` for a different configured origin. Android requires WebView's `WEB_MESSAGE_LISTENER` and `DOCUMENT_START_SCRIPT` features; registration fails explicitly on unsupported WebView implementations. Do not use wildcard origins for remotely navigable WebViews.
114
-
115
- ## Ionic overlay support (optional)
116
-
117
- For an Ionic application, call this once during application startup. When calling it directly from the application, install `@webview-native-ui/engine` as a direct application dependency:
118
-
119
- ```ts
120
- import { enableSupportIonicOverlays } from '@webview-native-ui/engine/ionic';
121
-
122
- enableSupportIonicOverlays();
123
- ```
124
-
125
- Plugin maintainers can include this snippet in their application setup instructions, or call it inside an existing JS initializer. `@rdlabo/capacitor-native-ui` already calls the same initializer internally; its users need no additional call. Non-Ionic applications omit it. Native registration alone does not install Ionic event listeners.
126
-
127
- One monitor is shared per document, including between the npm module and the runtime injected by Swift/Android. Repeated initialization does not add listeners or erase explicit configuration. Destroying one projection removes only its subscription; it does not stop the monitor used by other plugins. The monitor lives with the document.
128
-
129
- The default covers Modal, Popover, Alert, Action Sheet, Loading, Picker, Toast and Menu lifecycle events. It compares each projection's DOM with the active overlay stack: content inside the front modal stays native, while covered content temporarily uses its prepared image. Toasts and inactive sheet backdrops do not cover a surface. Nested overlays keep it covered until the remaining stack permits restoration. Missing images retain the normal Web fallback. This temporary coverage never overwrites a plugin's explicit `web` or `none` destination. Projections registered later read the current state.
130
-
131
- Native Popover ownership is supplied by the Native UI vertical-bars renderer. The shared monitor does not treat its source DOM as a covering Web overlay. If native presentation fails, the renderer releases ownership and normal DOM coverage resumes.
132
-
133
- Optional settings are document-wide:
134
-
135
- ```ts
136
- enableSupportIonicOverlays({
137
- exclude: '[data-nonblocking-overlay]',
138
- additional: [
139
- {
140
- selector: 'app-dialog',
141
- events: {
142
- willPresent: 'appDialogWillPresent',
143
- didDismiss: 'appDialogDidDismiss',
144
- },
145
- },
146
- ],
147
- });
148
- ```
149
-
150
- Use `covers(overlay, target)` only when a registered DOM projection needs a custom coverage rule; return `undefined` for the default, or a boolean to override it. Exclusions and explicit native-window ownership take precedence. The standard relationship uses overlay order, DOM containment and backdrop state, rather than attempting to interpret arbitrary CSS stacking contexts. Configure unusual layouts explicitly.
151
-
152
- ## Low-level Web projection
153
-
154
- 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`.
155
-
156
- ```ts
157
- import { createWebProjectionScope, createWebProjection } from '@webview-native-ui/engine';
158
-
159
- const fallback = container.querySelector<HTMLElement>('[data-web-fallback]');
160
- if (!fallback) throw new Error('Web fallback is missing');
161
- const scope = createWebProjectionScope(fallback, 'my-plugin');
162
- const projection = await createWebProjection(
163
- scope,
164
- {
165
- send: (request) => plugin.snapshot(request),
166
- listen: (listener) => plugin.addListener('snapshotInteraction', listener),
167
- },
168
- {
169
- wake: () => schedulePluginUpdate(),
170
- blocked: () => overlayIsActive(),
171
- revision: () => lastSentRevision,
172
- },
173
- );
174
- ```
175
-
176
- 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.
177
-
178
- 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.
179
- 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.
180
- 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.
181
- 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.
182
- 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.
183
- 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.
184
-
185
- 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.
186
-
187
- 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.
188
-
189
- The image is absolutely positioned within the fallback, which retains its layout box. As with the previous pseudo-element presentation, 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.
190
-
191
- 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.
192
-
193
- ### Notifications from the existing plugin
194
-
195
- The low-level manager can subscribe to the existing plugin's events through `options.events`:
196
-
197
- ```ts
198
- events: {
199
- addListener: (name, listener) => plugin.addListener(name, listener),
200
- update: ['setColors', 'setTabs', 'tabSelected'],
201
- projection: {
202
- native: ['shown'],
203
- web: ['transitionStarted'],
204
- none: ['hidden'],
205
- },
206
- }
207
- ```
208
-
209
- 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`.
210
-
211
- 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.
212
-
213
- The manager converts private native file URLs with Capacitor internally. Plugin authors do not supply a URL resolver.
214
-
215
- This section describes the lower-level manager for renderers that already own batch reconciliation, such as the Ionic adapter. Prefer the native registration in the plugin guides for a new integration; it owns startup, measurements and draw acknowledgements.
216
-
217
- ### Multiple fallbacks in one Native transaction
218
-
219
- 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.
220
-
221
- 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.
222
-
223
- `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.
224
-
225
- ### Native handoff on both platforms
226
-
227
- Call all view and snapshot lifecycle APIs on the main/UI thread. Native input frames use **points relative to the WebView on iOS** and **physical pixels relative to the reference view on Android**. `viewportWidth` is the Web viewport width in CSS pixels. Prepared parts sent to JavaScript are converted to CSS pixels.
228
-
229
- On both platforms, `cover` installs the prepared entry image **before hiding the supplied live views**, within the same UI turn. Its boolean result reports whether this was possible; failure leaves visibility unchanged. Supply the actual views for every captured component. Do not separately hide views or create another entry copy.
230
-
231
- ```swift
232
- let shown = snapshots.cover(token, in: webView, views: renderedViews, transient: movingSurface, frames: destinationFrames)
233
- ```
234
-
235
- ```kotlin
236
- val shown = snapshots.cover(token, activity.window, webView, renderedViews, transient = movingSurface, frames = destinationFrames)
237
- ```
238
-
239
- For a custom transport, convert request `parts` from CSS pixels to native frames using `webView.bounds.width / viewportWidth` on iOS or `webView.width / viewportWidth` on Android, mapping each part's IDs to its rectangle. Pass these as `destinationFrames` for both `cover` and `restore`. A retained image can move before its first cover or after a failed return copy; always use the supplied coordinates. When `parts` is absent, pass `nil` / `null` to retain the captured placement. The high-level `NativeUIProjection` registration handles this conversion automatically.
240
-
241
- A transient surface keeps its image while its sources retain the same sizes and positions relative to their projection scopes. Moving the whole scope can reuse pixels; moving a source inside a fixed scope invalidates the cached layout.
242
-
243
- `transient: true` is for a moving foreground surface: it does not replace the background's `coveringToken`. Retire the entry copy with `retireCopy(token)` after Web image readiness. Before applying a normal native update, undo the engine's visibility changes with iOS `restoreControls()` / Android `restoreViews()`. For return, `restore` installs the cached return image, the plugin renders underneath it, and `retireCopy(token, entry: false)` retires it after the corresponding acknowledgement/readiness. These platform-native names remain idiomatic; the lifecycle is the same. `cover` is an entry operation, not a method to repeat on each normal update.
244
-
245
- Token cleanup uses `release(token)`; owner teardown uses `reset()` (and Android `close()` when disposing its encoder). A surface manager must not reset another surface's captures. Web fallback remains the renderer's responsibility when a capture or native placement is unavailable.
246
-
247
- The optional `eligible(source)` and `current(sources)` callbacks keep framework-specific surface selection outside the engine. `transient: true` supports a moving surface whose return image must be rebased. Arbitrary CSS transforms are not supported by native scroll tracking; such movement can use this image path.
248
-
249
- The image transport accepts `WebProjectionRequest` and returns prepared token/parts or `{ shown: false }`. Parts use local `file:` URLs and CSS-pixel viewport coordinates. Requests use the plugin's existing ordered bridge:
250
-
251
- | Action | Native responsibility |
252
- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
253
- | `prepare` | Validate the current revision and IDs, then `NativeUICoveredSnapshots.prepare` using actual rendered frames. |
254
- | `cover` | Validate the token and complete native surface ownership; create the entry copy at supplied frames (or captured frames when absent) and hide the associated native views. Return `shown: false` on failure. |
255
- | `painted` | Retire only that token's entry copy. |
256
- | `restore` | Rebase the prepared return copy to supplied frames. Never capture here. |
257
- | `restored` | Retire only that token's return copy after the native update ACK and Web paint boundary. |
258
- | `release` | Release the named token, files and associated temporary views. |
259
- | `reset` | Owner teardown only; release all native capture resources. |
260
-
261
- 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.
262
-
263
- ### Temporary storage and application permissions
264
-
265
- Prepared images stay in the app's private temporary storage. iOS uses `FileManager.default.temporaryDirectory`. On Android, construct `NativeUISnapshots` with a directory inside `context.cacheDir`; do not use shared or external storage. Neither platform needs a photo-library or storage permission prompt for this path. No Capacitor Filesystem plugin is required. See [Apple's sandbox directories](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html) and [Android's app-specific storage](https://developer.android.com/training/data-storage/app-specific).
266
-
267
- `Capacitor.convertFileSrc` maps the returned file URL to the existing WebView local server. It grants no filesystem permission and does not upload the image. If the application sets a Content Security Policy, its `img-src` must allow the local app origin used by that URL; do not enable broad `file:` access or unrelated WebView file-access settings.
268
-
269
- `release(token)` and owner teardown remove owned snapshot files. Abrupt process termination can leave temporary images until the operating system clears the cache or the app is uninstalled; immediate deletion after a crash is not guaranteed. Keep this storage private and treat it as disposable, not persistent application data.
270
-
271
- ## Native scroll tracking
272
-
273
- `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.
274
-
275
- 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.
276
-
277
- 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.
278
-
279
- 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.
280
-
281
- ## Android library
282
-
283
- Android uses a Gradle Android library, distributed as an AAR through Maven rather than Capacitor plugin registration. The publication coordinates are `io.github.webview-native-ui:engine:<version>`. Build and verify the local Maven publication with:
284
-
285
- ```sh
286
- android/gradlew -p android testDebugUnitTest assembleRelease connectedDebugAndroidTest publishReleasePublicationToVerificationRepository
287
- ```
288
-
289
- `NativeUIClippedView` is a low-level helper that applies caller-supplied position and clipping without resetting unchanged dimensions. `NativeUIProjection` manages image handoff and restoration; it does not use this helper to lay out the renderer. The consuming plugin owns normal native placement and size updates. Android does not claim WKWebView-style native overflow binding. High-level registrations share a non-consuming window touch observer: down cancels in-flight capture adoption, up/cancel/focus loss permits preparation again, and teardown removes only that registration. Existing callback dispatch and return values are preserved.
290
-
291
- `NativeUISnapshots` prepares composited PixelCopy images and local PNG files, then covers/restores from that cache. Encoding runs off the UI thread; only one preparation runs per owner. Notify it of visual changes and interaction, and release tokens on teardown. All view/lifecycle calls run on the UI thread. The plugin retains revision, eligibility and fallback policy.
292
-
293
- After prepared Web pixels are shown or retired, `NativeUIWebReadiness.prepare` checks readiness for a subsequent WebView draw. Its `current` predicate must validate the owning session, source generation and image revision. The temporary native copy covers this asynchronous handoff; overlay startup itself does not wait. Neither bridge acknowledgement nor this callback proves physical display presentation.
294
-
295
- [AdMob PR #487](https://github.com/capacitor-community/admob/pull/487) demonstrates a persistent image underlay for opaque test ads. That policy is not enabled globally: translucent UI would be composited twice, and video/SDK interactions require renderer-specific decisions. Never synthesize clicks through an image. The Native UI consumer uses the engine on both iOS and Android. Android acceptance tests exercise the real Capacitor bridge and translucent Compose content. Other SDK consumers still require their own integration and acceptance testing.
296
-
297
- ### Translucent Android surfaces
298
-
299
- Capture the composited window pixels, including the background, instead of drawing the translucent view onto an empty bitmap. Keep the prepared Web image decoded but hidden. At handoff, `cover(token, window, reference, views)` installs a temporary opaque image copy before hiding those native views in the same UI turn. Reveal the Web image, confirm Web drawing readiness, then retire the entry copy. The overlay starts immediately; image encoding is never on this path.
300
-
301
- For return, install the prepared return copy with `restore`, restore the native views underneath it, hide the Web image, and retire the return copy after Web drawing readiness. This avoids compositing live translucent native pixels over their own snapshot. Validate the token and current surface generation at every asynchronous callback. The temporary copy is noninteractive and bounded; teardown releases both copies and cached resources.
302
-
303
- This preserves the captured background, not a continuously changing scene behind the image. Background changes must invalidate the cache through the same visual-change notifications as content updates. Video and unsupported capture surfaces retain the consumer's fallback policy.
304
-
305
- Consumer integration tests belong to their respective repositories. Engine CI exercises its browser, Swift and Android contracts without requiring a sibling renderer checkout.
306
-
307
- ### Final placement when returning from an opaque image (Android)
308
-
309
- A plugin that normally measures and posts native placement on every scroll can opt
310
- into a final placement handoff. Pass `updatePlacement: (JSONObject) -> Unit` to its
311
- Android `NativeUIProjection` registration and bind its Web host with
312
- `bindProjectionPlacement(element, measure, invalidate)`.
313
-
314
- - `binding.projected` becomes true only after native confirms the image handoff.
315
- While true, the plugin can reuse that host's last placement in full placement
316
- batches and skip measuring it. Other hosts continue their ordinary updates.
317
- - `measure()` returns a JSON object synchronously. The engine includes it in the
318
- existing restore request (or an active-image recovery request). Native validates
319
- the active image token and View identity, calls `updatePlacement`, then reveals
320
- native and acknowledges its draw. No extra bridge round trip is introduced.
321
- - The plugin must validate its own identity, generation and per-host sequence in
322
- `updatePlacement`. Throw to reject a stale or invalid placement. A final update
323
- for one host must not cause updates for another host to be dropped.
324
- - `invalidate()` is called after a failed handoff request. Clear optimistic
325
- placement/deduplication caches and schedule ordinary placement reconciliation.
326
- - Dispose the binding when detaching a host. Session ownership prevents an old
327
- registration's delayed acknowledgement from clearing a new registration's
328
- projection state. Pause, author visibility and layout changes must still use
329
- the plugin's normal visibility/invalidation paths.
330
-
331
- This opt-in does not change registrations without `updatePlacement`. Android rejects
332
- `updatePlacement` unless `captureMode = CaptureMode.VIEW`; live video and other ineligible captures
333
- continue native placement tracking under `failedFallback = NATIVE`.
334
-
335
- ## Capture timing
336
-
337
- `captureDelayMs` is an optional registration setting shared by Capacitor and Cordova.
338
- It defaults to **50 milliseconds** on iOS, Android and the lower-level `createWebProjection`
339
- options. Values must be finite and nonnegative. It uses the existing debounce: a new
340
- update or interaction restarts preparation after activity settles. It does not add a
341
- second timer or delay the overlay's presentation.
342
-
343
- Until replacement pixels are ready, the last completed image remains available. If
344
- there is no completed image, normal Web fallback applies. Opening an overlay during
345
- the debounce does not wait for a new capture. Consequently, increasing the delay also
346
- increases the period in which the previous appearance may be used after an update.
347
-
348
- A starting recommendation for **UITabBar Liquid Glass selection animations is 600 ms**.
349
- In iOS 27.0 iPhone and iOS 27.1 Duo simulator comparisons, 50 ms captured an enlarged
350
- selection, 300 ms still retained a small residual change, and 600 ms matched the settled
351
- appearance observed at 1000 ms. This is a recommendation for that interaction, not an
352
- Apple timing contract or a universal Liquid Glass duration. Measure your control's
353
- selection, reselection and successive updates before choosing a value. Continuously
354
- changing content has no guaranteed settled snapshot.
355
-
356
- ## Image color precision on iOS
357
-
358
- Prepared window snapshots use an extended Display P3 Float16 bitmap during capture
359
- and are encoded as 16-bit Display P3 PNGs on a background queue. The Web projection
360
- displays the prepared image directly, without a Canvas or WebGPU conversion. Native
361
- entry and return copies also use the decoded PNG, so both sides share the same
362
- prepared pixels after color conversion. Encoding and decoding
363
- stay outside the overlay handoff; consumers do not need a new setting or dependency.
364
-
365
- This reduces color loss in wide-gamut content but does not guarantee identical Liquid
366
- Glass appearance: native compositing and conversion to integer PNG can still change
367
- pixels. Higher precision also increases capture memory and image size. The existing
368
- fallback policy applies if preparation fails. Android retains its platform capture path.