@webview-native-ui/engine 0.0.1 → 0.0.2
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/LICENSE +17 -14
- package/README.md +27 -599
- package/WebViewNativeUIEngine.podspec +2 -2
- package/dist/{scroll-bindings.d.ts → dom/scroll-bindings.d.ts} +1 -1
- package/dist/{scroll-placement.d.ts → dom/scroll-placement.d.ts} +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +8 -8
- package/dist/ionic.d.ts +3 -3
- package/dist/ionic.js +1 -1
- package/dist/{native-bootstrap.d.ts → native/bootstrap.d.ts} +1 -1
- package/dist/{native-bootstrap.js → native/bootstrap.js} +1 -1
- package/dist/{bridge.d.ts → native/bridge.d.ts} +1 -1
- package/dist/{native-session.d.ts → native/session.d.ts} +3 -2
- package/dist/{native-session.js → native/session.js} +8 -7
- package/dist/{projection-events.d.ts → projection/events.d.ts} +1 -1
- package/dist/{projection-events.js → projection/events.js} +1 -1
- package/dist/{web-projection.d.ts → projection/web.d.ts} +5 -3
- package/dist/{web-projection.js → projection/web.js} +9 -6
- package/dist/types.d.ts +1 -1
- package/docs/assets/hero.png +0 -0
- package/docs/capacitor-plugin.md +279 -0
- package/docs/cordova-plugin.md +198 -0
- package/docs/cordova.md +1 -123
- package/docs/maintenance.md +52 -0
- package/docs/projection.md +340 -0
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +6 -3
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -3
- package/package.json +2 -3
- package/NOTICE +0 -39
- /package/dist/{geometry.d.ts → dom/geometry.d.ts} +0 -0
- /package/dist/{geometry.js → dom/geometry.js} +0 -0
- /package/dist/{overlays.d.ts → dom/overlays.d.ts} +0 -0
- /package/dist/{overlays.js → dom/overlays.js} +0 -0
- /package/dist/{scroll-bindings.js → dom/scroll-bindings.js} +0 -0
- /package/dist/{scroll-placement.js → dom/scroll-placement.js} +0 -0
- /package/dist/{visibility.d.ts → dom/visibility.d.ts} +0 -0
- /package/dist/{visibility.js → dom/visibility.js} +0 -0
- /package/dist/{bridge.js → native/bridge.js} +0 -0
- /package/dist/{runtime.d.ts → native/resource-url.d.ts} +0 -0
- /package/dist/{runtime.js → native/resource-url.js} +0 -0
- /package/dist/{projection-placement.d.ts → projection/placement.d.ts} +0 -0
- /package/dist/{projection-placement.js → projection/placement.js} +0 -0
- /package/dist/{web-projection-scope.d.ts → projection/scope.d.ts} +0 -0
- /package/dist/{web-projection-scope.js → projection/scope.js} +0 -0
- /package/dist/{snapshot-styles.d.ts → projection/styles.d.ts} +0 -0
- /package/dist/{snapshot-styles.js → projection/styles.js} +0 -0
package/README.md
CHANGED
|
@@ -1,624 +1,52 @@
|
|
|
1
1
|
# WebView Native UI Engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Bring Web UI above the native layer.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
_Visually, through seamless native-to-Web projection._
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+

|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
_Conceptual view. Native appearance is projected into the Web layer during overlay presentation._
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
| ------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
13
|
-
| DOM measurement, image geometry and supported native scroll binding | Creating, attaching, positioning and sizing native views |
|
|
14
|
-
| Startup, event subscriptions and native draw acknowledgements | Providing the fallback DOM at the correct layer |
|
|
15
|
-
| Prepared image cache, decoding, display and retirement | Reporting appearance changes and the intended projection destination |
|
|
16
|
-
| Native capture and temporary return images | Providing native views and SDK capture eligibility |
|
|
17
|
-
| Image invalidation, token cleanup and observer teardown | Owning business actions and the interactive Web fallback |
|
|
11
|
+
Build WebView apps where native controls and Web content share the same scrolling screens, transitions and overlays.
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
## Beyond the WebView boundary
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
WebView apps combine the Web's flexible layouts with access to native platform capabilities. But native UI and the Web page live in separate rendering layers. A native control placed above the WebView does not automatically participate in DOM scrolling, clipping or stacking order. A Web modal cannot simply cover it with a higher `z-index`.
|
|
22
16
|
|
|
23
|
-
|
|
17
|
+
Crossing that boundary takes more than a bridge call. It requires coordinating placement, appearance, input, rendering and restoration across two rendering systems on each platform. Rebuilding that coordination for every component makes otherwise useful designs expensive to deliver and maintain.
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
27
|
-
| `native` | Show the interactive native view at confirmed geometry. During motion, use supported native tracking or a prepared image. |
|
|
28
|
-
| `web` | Keep native content below the covering Web layer by removing its native projection; use a prepared image or the plugin's Web fallback. |
|
|
29
|
-
| `none` | Retire native content, projected images and fallback content. Later update/capture callbacks must not show it again. |
|
|
19
|
+
WebView Native UI Engine makes that work reusable. It lets applications combine native UI with Web-driven screens and interactions, while sharing the machinery that moves presentation between the two layers. The goal is to expand what a WebView app can be built to do—and make that practical to ship.
|
|
30
20
|
|
|
31
|
-
|
|
21
|
+
## What this makes possible
|
|
32
22
|
|
|
33
|
-
|
|
23
|
+
- **Native UI within Web-designed screens.** Combine native toolbars, tab bars, player controls and eligible SDK views with the application's DOM layout and navigation.
|
|
24
|
+
- **Web overlays that coexist with native content.** Present modals and other overlays over native surfaces, then return to the interactive native UI when they close.
|
|
25
|
+
- **Scrolling and transitions that retain native appearance.** Carry the native surface's captured appearance through Web-layer movement, without recreating its styling in CSS.
|
|
26
|
+
- **Shared infrastructure across components and platforms.** Reuse the same projection and restoration lifecycle on iOS and Android instead of implementing it independently in every integration.
|
|
34
27
|
|
|
35
|
-
|
|
28
|
+
## The approach
|
|
36
29
|
|
|
37
|
-
|
|
30
|
+
Use the live native view where it belongs, and a prepared visual projection in the DOM when the Web layer needs to own presentation. Restore the interactive native view when it can take over again. An existing DOM container gives the Web projection its place in the application's layout and layer structure.
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
The Engine coordinates this handoff; the integrating renderer still creates and positions its views. A projected image preserves appearance, not live interaction or video. Capture availability and layout support determine when a projection can be used, with an explicit fallback when it cannot. See [projection behavior](docs/projection.md) for the detailed contract and limits.
|
|
40
33
|
|
|
41
|
-
|
|
34
|
+
## Integration
|
|
42
35
|
|
|
43
|
-
The
|
|
36
|
+
Applications gain these capabilities through plugins that integrate the Engine. Capacitor and Cordova plugin authors add it as a library dependency while retaining their existing native controls and public APIs. The same Engine supports iOS and Android. Ionic and Angular are not required; Ionic overlay coordination is optional.
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
3. The plugin emits notifications after its updates and releases the registration at teardown.
|
|
38
|
+
- [Capacitor plugin guide](docs/capacitor-plugin.md) — install dependencies and add registration, events and teardown to your existing plugin.
|
|
39
|
+
- [Cordova plugin guide](docs/cordova-plugin.md) — integrate the same Engine with Cordova's lifecycle and resource handler, without a Capacitor dependency.
|
|
48
40
|
|
|
49
|
-
|
|
41
|
+
Native registration starts the Web runtime; applications do not need a separate Engine initializer for the base integration. Keep your plugin's normal API and notifications.
|
|
50
42
|
|
|
51
|
-
|
|
43
|
+
The current API is pre-1.0. Use matching, pinned Engine versions across npm and native dependencies. Android placement requires API 23+, with composited snapshots available from API 26+.
|
|
52
44
|
|
|
53
|
-
|
|
45
|
+
## Further reading
|
|
54
46
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```diff
|
|
58
|
-
import Capacitor
|
|
59
|
-
import UIKit
|
|
60
|
-
+import WebViewNativeUIEngine
|
|
61
|
-
|
|
62
|
-
@objc(ExamplePlugin)
|
|
63
|
-
public class ExamplePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
64
|
-
public let identifier = "ExamplePlugin"
|
|
65
|
-
public let jsName = "Example"
|
|
66
|
-
public let pluginMethods: [CAPPluginMethod] = [
|
|
67
|
-
CAPPluginMethod(name: "setColors", returnType: CAPPluginReturnPromise)
|
|
68
|
-
]
|
|
69
|
-
private var nativeView: UIView?
|
|
70
|
-
+ private var projection: NativeUIProjection?
|
|
71
|
-
|
|
72
|
-
public override func load() {
|
|
73
|
-
// Existing code creates and attaches nativeView here.
|
|
74
|
-
+ do {
|
|
75
|
-
+ projection = try NativeUIProjection(
|
|
76
|
-
+ webView: bridge?.webView,
|
|
77
|
-
+ plugin: jsName,
|
|
78
|
-
+ projectionSelector: "#native-ui-fallback",
|
|
79
|
-
+ receiveListeners: .init(
|
|
80
|
-
+ update: ["appearanceChanged"],
|
|
81
|
-
+ projection: .init(
|
|
82
|
-
+ native: ["shown"],
|
|
83
|
-
+ web: ["transitionStarted"],
|
|
84
|
-
+ none: ["hidden"]
|
|
85
|
-
+ )
|
|
86
|
-
+ ),
|
|
87
|
-
+ view: { [weak self] in
|
|
88
|
-
+ self?.nativeView
|
|
89
|
-
+ }
|
|
90
|
-
+ )
|
|
91
|
-
+ } catch {
|
|
92
|
-
+ CAPLog.print("Native UI projection unavailable: \(error)")
|
|
93
|
-
+ }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
@objc func setColors(_ call: CAPPluginCall) {
|
|
97
|
-
DispatchQueue.main.async {
|
|
98
|
-
// Existing code applies the requested colors to nativeView.
|
|
99
|
-
+ self.notifyListeners("appearanceChanged", data: [:])
|
|
100
|
-
call.resolve()
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
+
|
|
104
|
-
+ public override func notifyListeners(_ eventName: String, data: [String: Any]?, retainUntilConsumed: Bool) {
|
|
105
|
-
+ projection?.receive(eventName, data: data)
|
|
106
|
-
+ super.notifyListeners(eventName, data: data, retainUntilConsumed: retainUntilConsumed)
|
|
107
|
-
+ }
|
|
108
|
-
+
|
|
109
|
-
+ deinit {
|
|
110
|
-
+ projection?.destroy()
|
|
111
|
-
+ }
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Pass `bridge?.webView` directly. If the WebView is unavailable, the Engine throws an initialization error into the same `catch`; a separate guard or force unwrap is unnecessary.
|
|
116
|
-
|
|
117
|
-
If the view is created later in an existing `initialize` / `show` method, put the registration there after view creation instead of in `load()`. Retain one registration for that renderer's lifetime; do not create another on every update. When replacing a renderer, destroy its old registration before registering the replacement.
|
|
118
|
-
|
|
119
|
-
### Android: add the registration to your `Plugin` class
|
|
120
|
-
|
|
121
|
-
Make the corresponding changes in `android/src/main/java/<your-package>/ExamplePlugin.kt`. **`pluginHandle.id`** is Capacitor's registered plugin name, derived from `@CapacitorPlugin`; no second name string is needed.
|
|
122
|
-
|
|
123
|
-
```diff
|
|
124
|
-
import android.view.View
|
|
125
|
-
import com.getcapacitor.Plugin
|
|
126
|
-
import com.getcapacitor.PluginCall
|
|
127
|
-
import com.getcapacitor.PluginMethod
|
|
128
|
-
import com.getcapacitor.annotation.CapacitorPlugin
|
|
129
|
-
+import android.util.Log
|
|
130
|
-
+import com.getcapacitor.JSObject
|
|
131
|
-
+import io.github.webview_native_ui.engine.NativeUIProjection
|
|
132
|
-
+import io.github.webview_native_ui.engine.NativeUIProjection.ReceiveListeners
|
|
133
|
-
|
|
134
|
-
@CapacitorPlugin(name = "Example")
|
|
135
|
-
class ExamplePlugin : Plugin() {
|
|
136
|
-
private var nativeView: View? = null
|
|
137
|
-
+ private var projection: NativeUIProjection? = null
|
|
138
|
-
|
|
139
|
-
override fun load() {
|
|
140
|
-
// Existing code creates and attaches nativeView here.
|
|
141
|
-
+ projection = runCatching {
|
|
142
|
-
+ NativeUIProjection.install(bridge.webView)
|
|
143
|
-
+ NativeUIProjection(
|
|
144
|
-
+ window = activity.window,
|
|
145
|
-
+ webView = bridge.webView,
|
|
146
|
-
+ plugin = pluginHandle.id,
|
|
147
|
-
+ projectionSelector = "#native-ui-fallback",
|
|
148
|
-
+ receiveListeners = ReceiveListeners(
|
|
149
|
-
+ update = listOf("appearanceChanged"),
|
|
150
|
-
+ projection = ReceiveListeners.Projection(
|
|
151
|
-
+ native = listOf("shown"),
|
|
152
|
-
+ web = listOf("transitionStarted"),
|
|
153
|
-
+ none = listOf("hidden")
|
|
154
|
-
+ )
|
|
155
|
-
+ ),
|
|
156
|
-
+ view = { nativeView }
|
|
157
|
-
+ )
|
|
158
|
-
+ }.onFailure {
|
|
159
|
-
+ Log.e(javaClass.simpleName, "Native UI projection unavailable", it)
|
|
160
|
-
+ }.getOrNull()
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
@PluginMethod
|
|
164
|
-
fun setColors(call: PluginCall) {
|
|
165
|
-
activity.runOnUiThread {
|
|
166
|
-
// Existing code applies the requested colors to nativeView.
|
|
167
|
-
+ notifyListeners("appearanceChanged", JSObject())
|
|
168
|
-
call.resolve()
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
+
|
|
172
|
-
+ override fun notifyListeners(eventName: String, data: JSObject, retainUntilConsumed: Boolean) {
|
|
173
|
-
+ projection?.receive(eventName, data)
|
|
174
|
-
+ super.notifyListeners(eventName, data, retainUntilConsumed)
|
|
175
|
-
+ }
|
|
176
|
-
+
|
|
177
|
-
+ override fun handleOnDestroy() {
|
|
178
|
-
+ projection?.close()
|
|
179
|
-
+ projection = null
|
|
180
|
-
+ super.handleOnDestroy()
|
|
181
|
-
+ }
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
On Android, call `NativeUIProjection.install(bridge.webView)` in plugin `load()`,
|
|
186
|
-
before the first navigation, even when SDK views arrive later. Include installation
|
|
187
|
-
in the same error boundary as registration; if it fails, keep the existing Web
|
|
188
|
-
fallback and do not retry installation after navigation. Registrations then
|
|
189
|
-
share that main-frame, origin-restricted channel; they do not install new message
|
|
190
|
-
listeners into a loaded WebView. This avoids a WebView native crash reproduced when
|
|
191
|
-
registering an asynchronously loaded AdMob view. The registration constructor can
|
|
192
|
-
install the channel only on an un-navigated WebView; late installation fails with
|
|
193
|
-
an actionable error instead of attempting unsafe injection. All registrations on
|
|
194
|
-
the same WebView must use the same allowed origins. Closing a registration removes
|
|
195
|
-
only its owner from the channel, so other slots continue working. The shared channel
|
|
196
|
-
remains installed for the WebView lifetime; registrations must still be retained
|
|
197
|
-
and closed by their consuming plugin.
|
|
198
|
-
|
|
199
|
-
Both diffs add a private property, native registration, a notification override, an update notification and teardown. They add no Engine methods to `pluginMethods`, no public JS wrapper, and no application-side Engine initializer.
|
|
200
|
-
|
|
201
|
-
Keep your plugin's existing `registerPlugin()` export. Use the same event names in the registration and `notifyListeners`; if the plugin already emits a suitable event (such as `tabSelected`), list that event instead of emitting it twice.
|
|
202
|
-
|
|
203
|
-
### Notifications during startup
|
|
204
|
-
|
|
205
|
-
The notification override above lets the Engine retain the latest matching projection destination before the Web runtime subscribes. No separate state variable or initial-state callback is needed. Forward every notification to `super` as usual: the Engine records state without replaying notifications, updates, or user actions. Without a destination notification, it infers the initial destination from native view visibility. Registration must exist before the notifications it should observe.
|
|
206
|
-
|
|
207
|
-
If your plugin owns multiple registrations, pass the notification to each registration; `receiveListeners.match` selects the owning surface. Appearance updates do not change the destination. The renderer still owns native placement and visibility before connection.
|
|
208
|
-
|
|
209
|
-
With the default Web fallback, scrolling without a prepared image or native scroll binding keeps the Web fallback visible until the renderer confirms current native placement with its native-destination notification. An appearance update alone is not that confirmation. Consumers choosing native failure recovery must keep their own native geometry current.
|
|
210
|
-
|
|
211
|
-
### Which events to emit
|
|
212
|
-
|
|
213
|
-
All incoming event declarations belong to `receiveListeners`. Its `update` list refreshes the image, its `projection` lists select the destination, and its optional `match` filters notifications for the owning surface.
|
|
214
|
-
|
|
215
|
-
| Notification | Where to emit it in the existing plugin |
|
|
216
|
-
| --------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
217
|
-
| `appearanceChanged` (an `update` event) | After a successful native appearance update, such as `setColors` |
|
|
218
|
-
| `transitionStarted` (`web`) | Before the fallback DOM starts moving or a Web layer covers the native view |
|
|
219
|
-
| `shown` (`native`) | After the plugin's existing show operation, or when returning from the Web projection |
|
|
220
|
-
| `hidden` (`none`) | After the plugin's existing hide operation |
|
|
221
|
-
|
|
222
|
-
The names in the examples are conventions you choose, not reserved Engine methods. An `update` event refreshes the prepared image without changing the requested destination. It does not call `setColors` again.
|
|
223
|
-
|
|
224
|
-
Emit `transitionStarted` before the DOM starts moving, `shown` when native projection is requested again, and `hidden` after the plugin's own hide operation. Native capture, URL conversion, image preparation, draw acknowledgement, image retirement and listener cleanup stay inside the engine. The engine never invokes the plugin's `show`, `hide`, selection or ad-loading methods. End the registration with `projection.destroy()` on iOS or `projection.close()` on Android when the renderer is destroyed. Retain the registration for its full lifetime and call native APIs on the UI thread.
|
|
225
|
-
|
|
226
|
-
`projectionSelector` is required and must resolve to **one existing container when projection starts**, including one retrieved by the plugin's own `querySelector`. The engine does not create a DOM 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.
|
|
227
|
-
|
|
228
|
-
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.
|
|
229
|
-
|
|
230
|
-
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 fallback content, including shadow content, must inherit the host's `visibility`: do not override it with `visibility: visible`, and reserve the host's `::before` for the projection image. These requirements apply to open and closed shadow roots.
|
|
231
|
-
|
|
232
|
-
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.
|
|
233
|
-
|
|
234
|
-
For SDK consumers without an equivalent Web fallback, set `failedFallback: .native`
|
|
235
|
-
on iOS or `failedFallback = NativeUIProjection.FailedFallback.NATIVE` on Android.
|
|
236
|
-
The default is `.web` / `WEB`, preserving existing consumers. If automatic motion
|
|
237
|
-
cannot use a prepared image (not ready, unsupported capture, or rejected handoff),
|
|
238
|
-
`native` preserves the live native view and keeps fallback DOM hidden. The consumer
|
|
239
|
-
must continue positioning that view; this option does not create native scroll
|
|
240
|
-
tracking on Android. Explicit `web`, `none`, and overlay coverage take precedence:
|
|
241
|
-
this option never raises a native view above an intentionally covering Web layer.
|
|
242
|
-
|
|
243
|
-
For opaque, software-renderable Android SDK views, use
|
|
244
|
-
`captureMode = NativeUIProjection.CaptureMode.VIEW` together with an explicit
|
|
245
|
-
`failedFallback` policy (`WEB` unless the consumer maintains native placement). This draws only the View's
|
|
246
|
-
pixels with no surrounding padding, so clipping a partially visible ad cannot bake
|
|
247
|
-
in a toolbar or adjacent content. Video, SurfaceView, translucent views, and content
|
|
248
|
-
requiring Window composition must keep the default `COMPOSITED` mode. The consumer
|
|
249
|
-
must provide a matching, clipped DOM host for the full opaque View; arbitrary native
|
|
250
|
-
outline masks, transforms, and custom clipping are not reproduced.
|
|
251
|
-
|
|
252
|
-
VIEW pixels are cached by native View identity, dimensions, and content revision.
|
|
253
|
-
Ordinary scrolling retains the decoded Web image and updates placement without
|
|
254
|
-
capture, PNG encoding, or image decode. The image covers the host border box at
|
|
255
|
-
local origin (0, 0), independently of native placement acknowledgment latency.
|
|
256
|
-
Opaque images remain painted beneath the live native View between scrolls.
|
|
257
|
-
Before hiding that View, the engine waits for WebView visual readiness and a draw;
|
|
258
|
-
VIEW creates no native entry or return image copies. Return acknowledgment waits
|
|
259
|
-
across the native draw and the following animation frame. A generation check prevents
|
|
260
|
-
a delayed cover callback from hiding content after restoration or suspension.
|
|
261
|
-
|
|
262
|
-
The native Bitmap is recycled after PNG encoding in VIEW mode; the PNG and decoded
|
|
263
|
-
Web image remain cached. Plugin update notifications advance the content revision:
|
|
264
|
-
notify an update whenever content changes without replacing the View. Replacing the
|
|
265
|
-
registration, WebView navigation, reset, or close retires the cache. Encoding stays
|
|
266
|
-
off the UI thread; View drawing and cache ownership stay on it, with the existing
|
|
267
|
-
8-million-pixel cap.
|
|
268
|
-
|
|
269
|
-
COMPOSITED retains native handoff copies for translucent content. Entry copies wait
|
|
270
|
-
for WebView visual readiness and a draw before retirement. If the 500 ms entry
|
|
271
|
-
deadline expires, the native View is restored before the copy is removed, and a late
|
|
272
|
-
paint acknowledgment is rejected.
|
|
273
|
-
|
|
274
|
-
Android VIEW consumers can read `isProjecting` on the UI thread after a successful
|
|
275
|
-
cover to defer same-size native placement work. Keep only the latest accepted placement;
|
|
276
|
-
continue validating sequence/generation, and process hide, resize and teardown immediately.
|
|
277
|
-
Supply `beforeNativeReveal` to apply that pending placement synchronously before normal
|
|
278
|
-
restoration, failure recovery, or active-image release reveals native pixels. The engine
|
|
279
|
-
clears `isProjecting` before invoking the callback, so applying placement must not defer
|
|
280
|
-
again. The callback may run without pending work and must be idempotent. Clear consumer
|
|
281
|
-
pending work on hide/destroy and validate the loaded View identity before applying it.
|
|
282
|
-
The callback does not replace content/size invalidation or JavaScript geometry tracking.
|
|
283
|
-
|
|
284
|
-
On Android, consumers whose asynchronous hide method promises that all pixels have
|
|
285
|
-
disappeared must call `projection.hide { completed -> ... }` and resolve only when
|
|
286
|
-
`completed` is true. Native handoff copies disappear immediately; completion waits
|
|
287
|
-
for synchronous Web-proxy retirement. A timeout reports failure, not success.
|
|
288
|
-
Call `projection.show()` when presenting again, in addition to normal plugin events;
|
|
289
|
-
this also preserves presentation intent if event subscription is still starting.
|
|
290
|
-
|
|
291
|
-
Scroll observation follows composed-tree ancestors, including assigned slots.
|
|
292
|
-
Non-composed scroll events inside an Ionic shadow tree are observed at their source;
|
|
293
|
-
applications do not add an extra listener or dispatch synthetic document events.
|
|
294
|
-
Listeners follow host reparenting and slot reassignment and are removed on teardown.
|
|
295
|
-
|
|
296
|
-
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.
|
|
297
|
-
|
|
298
|
-
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.
|
|
299
|
-
|
|
300
|
-
## Ionic overlay support (optional)
|
|
301
|
-
|
|
302
|
-
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:
|
|
303
|
-
|
|
304
|
-
```ts
|
|
305
|
-
import { enableSupportIonicOverlays } from '@webview-native-ui/engine/ionic';
|
|
306
|
-
|
|
307
|
-
enableSupportIonicOverlays();
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
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.
|
|
311
|
-
|
|
312
|
-
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.
|
|
313
|
-
|
|
314
|
-
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.
|
|
315
|
-
|
|
316
|
-
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.
|
|
317
|
-
|
|
318
|
-
Optional settings are document-wide:
|
|
319
|
-
|
|
320
|
-
```ts
|
|
321
|
-
enableSupportIonicOverlays({
|
|
322
|
-
exclude: '[data-nonblocking-overlay]',
|
|
323
|
-
additional: [
|
|
324
|
-
{
|
|
325
|
-
selector: 'app-dialog',
|
|
326
|
-
events: {
|
|
327
|
-
willPresent: 'appDialogWillPresent',
|
|
328
|
-
didDismiss: 'appDialogDidDismiss',
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
],
|
|
332
|
-
});
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
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.
|
|
336
|
-
|
|
337
|
-
## Cordova
|
|
338
|
-
|
|
339
|
-
The same Engine supports Cordova plugins without installing Capacitor. Select `runtime: .cordova` (Swift) or `runtime = Runtime.CORDOVA` (Kotlin) and connect the host lifecycle/resource handler. See the [Cordova plugin integration guide](docs/cordova.md). The projection implementation is shared; there is no separate Cordova package.
|
|
340
|
-
|
|
341
|
-
## Install inside a plugin
|
|
342
|
-
|
|
343
|
-
```sh
|
|
344
|
-
npm install --save-exact @webview-native-ui/engine@0.0.1
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
The npm archive contains TypeScript declarations, ESM, Swift sources and a podspec. Native dependencies must also be declared by the consuming plugin; an npm dependency alone links neither the Swift library nor the Android AAR.
|
|
348
|
-
|
|
349
|
-
The primary native distribution is Swift Package Manager. Depend on the repository-root library from your plugin:
|
|
350
|
-
|
|
351
|
-
```swift
|
|
352
|
-
.package(url: "https://github.com/webview-native-ui/engine.git", exact: "0.0.1")
|
|
353
|
-
// In the plugin target's dependencies:
|
|
354
|
-
.product(name: "WebViewNativeUIEngine", package: "engine")
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
The new coordinates start at `0.0.1`. While the API is pre-1.0, pin the same Engine release across npm, SwiftPM, Gradle and CocoaPods, and upgrade deliberately. Applications should retain their dependency lockfiles for reproducible builds.
|
|
358
|
-
|
|
359
|
-
SwiftPM resolves this repository's version tags such as `v0.0.1`. A standalone release tag must exist before consumers switch to this URL; old `ios27-v…` theme tags are not Engine versions. For unreleased integration tests, use a local checkout or an explicit tested commit.
|
|
360
|
-
|
|
361
|
-
`import WebViewNativeUIEngine` requires no Capacitor registration. The npm companion supplies DOM measurement and Web image handoff; SwiftPM resolves the native dependency independently of npm hoisting.
|
|
362
|
-
|
|
363
|
-
### Android: add the Maven dependency
|
|
364
|
-
|
|
365
|
-
In the consuming plugin's `android/build.gradle`, add the Engine version matching its npm dependency:
|
|
366
|
-
|
|
367
|
-
```groovy
|
|
368
|
-
dependencies {
|
|
369
|
-
implementation 'io.github.webview-native-ui:engine:0.0.1'
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
Ensure `google()` and `mavenCentral()` are present in the consuming build's dependency repositories. For a build using centralized repositories, merge them into `android/settings.gradle`:
|
|
374
|
-
|
|
375
|
-
```groovy
|
|
376
|
-
dependencyResolutionManagement {
|
|
377
|
-
repositories {
|
|
378
|
-
google()
|
|
379
|
-
mavenCentral()
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
If the build uses project-level repositories instead, add them to its existing `repositories` block. Installing the npm package does not add this Gradle dependency. Use a version whose POM and AAR are available from Central; `PUBLISHING` only confirms acceptance and does not yet guarantee downloads.
|
|
385
|
-
|
|
386
|
-
### CocoaPods compatibility through npm
|
|
387
|
-
|
|
388
|
-
CocoaPods uses the Swift sources and podspec shipped in npm; no CocoaPods Trunk publication is required. The plugin maintainer declares the matching dependency in the plugin's podspec:
|
|
389
|
-
|
|
390
|
-
```ruby
|
|
391
|
-
s.dependency 'WebViewNativeUIEngine', '0.0.1'
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
The maintainer must also document this one-time application setup. Install the matching engine version as a direct npm dependency, then add the local pod **inside the application's target, outside `def capacitor_pods`**, which Capacitor regenerates:
|
|
395
|
-
|
|
396
|
-
```sh
|
|
397
|
-
npm install --save-exact @webview-native-ui/engine@0.0.1
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
```ruby
|
|
401
|
-
# ios/App/Podfile; adjust the path for a different native project layout.
|
|
402
|
-
target 'App' do
|
|
403
|
-
capacitor_pods
|
|
404
|
-
pod 'WebViewNativeUIEngine', :path => '../../node_modules/@webview-native-ui/engine'
|
|
405
|
-
end
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
Run `npx cap sync ios` and rebuild. Keep the direct engine version aligned with the consuming plugin's dependency when upgrading. With a nonstandard node_modules layout, point `:path` at the installed engine package directory. An npm dependency alone does not register a Pod, and a podspec dependency cannot specify this local path. The engine intentionally has no Capacitor plugin registration or Podfile rewriting hook. SwiftPM consumers do not need this CocoaPods setup.
|
|
409
|
-
|
|
410
|
-
## Low-level Web projection
|
|
411
|
-
|
|
412
|
-
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`.
|
|
413
|
-
|
|
414
|
-
```ts
|
|
415
|
-
import { createWebProjectionScope, createWebProjection } from '@webview-native-ui/engine';
|
|
416
|
-
|
|
417
|
-
const fallback = container.querySelector<HTMLElement>('[data-web-fallback]');
|
|
418
|
-
if (!fallback) throw new Error('Web fallback is missing');
|
|
419
|
-
const scope = createWebProjectionScope(fallback, 'my-plugin');
|
|
420
|
-
const projection = await createWebProjection(
|
|
421
|
-
scope,
|
|
422
|
-
{
|
|
423
|
-
send: (request) => plugin.snapshot(request),
|
|
424
|
-
listen: (listener) => plugin.addListener('snapshotInteraction', listener),
|
|
425
|
-
},
|
|
426
|
-
{
|
|
427
|
-
wake: () => schedulePluginUpdate(),
|
|
428
|
-
blocked: () => overlayIsActive(),
|
|
429
|
-
revision: () => lastSentRevision,
|
|
430
|
-
},
|
|
431
|
-
);
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
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.
|
|
435
|
-
|
|
436
|
-
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.
|
|
437
|
-
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.
|
|
438
|
-
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.
|
|
439
|
-
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.
|
|
440
|
-
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.
|
|
441
|
-
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.
|
|
442
|
-
|
|
443
|
-
The engine uses a noninteractive, empty CSS `::before` pseudo-element for the prepared image. Supply a container that supports it, such as an existing `div`, `button` or custom element. Replaced/void elements such as `img`, `input` and `video` are rejected when creating the scope; use their existing fallback container. It does not clone, replace, remove or set `inert`/`aria-hidden` on the existing fallback or its children. Removing its temporary image styles restores the same live DOM, including application changes made during projection. Event listeners and form values are preserved. The renderer may move that same element between parents; it must not replace the scope's element or let two managers control it simultaneously.
|
|
444
|
-
|
|
445
|
-
### Notifications from the existing plugin
|
|
446
|
-
|
|
447
|
-
The low-level manager can subscribe to the existing plugin's events through `options.events`:
|
|
448
|
-
|
|
449
|
-
```ts
|
|
450
|
-
events: {
|
|
451
|
-
addListener: (name, listener) => plugin.addListener(name, listener),
|
|
452
|
-
update: ['setColors', 'setTabs', 'tabSelected'],
|
|
453
|
-
projection: {
|
|
454
|
-
native: ['shown'],
|
|
455
|
-
web: ['transitionStarted'],
|
|
456
|
-
none: ['hidden'],
|
|
457
|
-
},
|
|
458
|
-
}
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
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`.
|
|
462
|
-
|
|
463
|
-
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.
|
|
464
|
-
|
|
465
|
-
The manager converts private native file URLs with Capacitor internally. Plugin authors do not supply a URL resolver.
|
|
466
|
-
|
|
467
|
-
This section describes the lower-level manager for renderers that already own batch reconciliation, such as the Ionic adapter. Prefer the native registration above for a new integration; it owns startup, measurements and draw acknowledgements.
|
|
468
|
-
|
|
469
|
-
### Multiple fallbacks in one Native transaction
|
|
470
|
-
|
|
471
|
-
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.
|
|
472
|
-
|
|
473
|
-
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.
|
|
474
|
-
|
|
475
|
-
`WebProjectionRequest` is an action-discriminated union. `prepare` requires `revision`, `viewportWidth` and `ids`; `cover` requires `token`; `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.
|
|
476
|
-
|
|
477
|
-
### Native handoff on both platforms
|
|
478
|
-
|
|
479
|
-
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.
|
|
480
|
-
|
|
481
|
-
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.
|
|
482
|
-
|
|
483
|
-
```swift
|
|
484
|
-
let shown = snapshots.cover(token, in: webView, views: renderedViews, transient: movingSurface)
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
```kotlin
|
|
488
|
-
val shown = snapshots.cover(token, activity.window, webView, renderedViews, transient = movingSurface)
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
`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.
|
|
492
|
-
|
|
493
|
-
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.
|
|
494
|
-
|
|
495
|
-
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.
|
|
496
|
-
|
|
497
|
-
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:
|
|
498
|
-
|
|
499
|
-
| Action | Native responsibility |
|
|
500
|
-
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
501
|
-
| `prepare` | Validate the current revision and IDs, then `NativeUICoveredSnapshots.prepare` using actual rendered frames. |
|
|
502
|
-
| `cover` | Validate the token and complete native surface ownership; create the entry copy and hide the associated native views. Return `shown: false` on failure. |
|
|
503
|
-
| `painted` | Retire only that token's entry copy. |
|
|
504
|
-
| `restore` | Rebase the prepared return copy to supplied frames. Never capture here. |
|
|
505
|
-
| `restored` | Retire only that token's return copy after the native update ACK and Web paint boundary. |
|
|
506
|
-
| `release` | Release the named token, files and associated temporary views. |
|
|
507
|
-
| `reset` | Owner teardown only; release all native capture resources. |
|
|
508
|
-
|
|
509
|
-
Use `NativeUICoveredSnapshots.observe` to notify the JS listener when interaction starts/ends. Captures use composited pixels so blur/refraction is preserved; they include the background under the clipped native component. 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.
|
|
510
|
-
|
|
511
|
-
### Temporary storage and application permissions
|
|
512
|
-
|
|
513
|
-
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).
|
|
514
|
-
|
|
515
|
-
`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.
|
|
516
|
-
|
|
517
|
-
`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.
|
|
518
|
-
|
|
519
|
-
## Native scroll tracking
|
|
520
|
-
|
|
521
|
-
`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.
|
|
522
|
-
|
|
523
|
-
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.
|
|
524
|
-
|
|
525
|
-
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.
|
|
526
|
-
|
|
527
|
-
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.
|
|
528
|
-
|
|
529
|
-
## Development
|
|
530
|
-
|
|
531
|
-
Use Node.js 24, matching CI. From this repository:
|
|
532
|
-
|
|
533
|
-
```sh
|
|
534
|
-
npm ci
|
|
535
|
-
npm run build
|
|
536
|
-
npm run lint
|
|
537
|
-
npx playwright install chromium
|
|
538
|
-
npm test
|
|
539
|
-
xcodebuild -scheme WebViewNativeUIEngine -destination 'platform=iOS Simulator,id=<booted simulator UUID>' test
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
Lint combines strict TypeScript checking, the standard ESLint/TypeScript recommended rules, and typed Promise checks for `src`. Scripts and tests receive syntax-level linting; Prettier owns formatting. `npm run format` applies formatting only. Bridge payloads and test doubles may use explicit `any`; production boundary validation remains necessary. An explicit `void` marks background work, but does not handle a rejected Promise. Android uses `lintRelease` with warnings treated as errors in CI; Swift is compiled and tested on an iOS simulator.
|
|
543
|
-
|
|
544
|
-
The engine is versioned independently of its consumers. Publish order is the native Maven artifact → npm engine; consumers upgrade separately. npm releases verify that the matching POM and AAR exist on Maven Central before publication. No publication is performed by build or test commands. Tests cover plain DOM, fractional/RTL geometry, stale ACKs, image ownership and native WKWebView scroll tracking; consumer tests remain responsible for Ionic/SDK integration.
|
|
545
|
-
|
|
546
|
-
## Android library
|
|
547
|
-
|
|
548
|
-
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:
|
|
549
|
-
|
|
550
|
-
```sh
|
|
551
|
-
android/gradlew -p android testDebugUnitTest assembleRelease connectedDebugAndroidTest publishReleasePublicationToVerificationRepository
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
`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.
|
|
555
|
-
|
|
556
|
-
`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.
|
|
557
|
-
|
|
558
|
-
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.
|
|
559
|
-
|
|
560
|
-
[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.
|
|
561
|
-
|
|
562
|
-
### Translucent Android surfaces
|
|
563
|
-
|
|
564
|
-
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.
|
|
565
|
-
|
|
566
|
-
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.
|
|
567
|
-
|
|
568
|
-
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.
|
|
569
|
-
|
|
570
|
-
Consumer integration tests belong to their respective repositories. Engine CI exercises its browser, Swift and Android contracts without requiring a sibling renderer checkout.
|
|
571
|
-
|
|
572
|
-
### Final placement when returning from an opaque image (Android)
|
|
573
|
-
|
|
574
|
-
A plugin that normally measures and posts native placement on every scroll can opt
|
|
575
|
-
into a final placement handoff. Pass `updatePlacement: (JSONObject) -> Unit` to its
|
|
576
|
-
Android `NativeUIProjection` registration and bind its Web host with
|
|
577
|
-
`bindProjectionPlacement(element, measure, invalidate)`.
|
|
578
|
-
|
|
579
|
-
- `binding.projected` becomes true only after native confirms the image handoff.
|
|
580
|
-
While true, the plugin can reuse that host's last placement in full placement
|
|
581
|
-
batches and skip measuring it. Other hosts continue their ordinary updates.
|
|
582
|
-
- `measure()` returns a JSON object synchronously. The engine includes it in the
|
|
583
|
-
existing restore request (or an active-image recovery request). Native validates
|
|
584
|
-
the active image token and View identity, calls `updatePlacement`, then reveals
|
|
585
|
-
native and acknowledges its draw. No extra bridge round trip is introduced.
|
|
586
|
-
- The plugin must validate its own identity, generation and per-host sequence in
|
|
587
|
-
`updatePlacement`. Throw to reject a stale or invalid placement. A final update
|
|
588
|
-
for one host must not cause updates for another host to be dropped.
|
|
589
|
-
- `invalidate()` is called after a failed handoff request. Clear optimistic
|
|
590
|
-
placement/deduplication caches and schedule ordinary placement reconciliation.
|
|
591
|
-
- Dispose the binding when detaching a host. Session ownership prevents an old
|
|
592
|
-
registration's delayed acknowledgement from clearing a new registration's
|
|
593
|
-
projection state. Pause, author visibility and layout changes must still use
|
|
594
|
-
the plugin's normal visibility/invalidation paths.
|
|
595
|
-
|
|
596
|
-
This opt-in does not change registrations without `updatePlacement`. Android rejects
|
|
597
|
-
`updatePlacement` unless `captureMode = CaptureMode.VIEW`; live video and other ineligible captures
|
|
598
|
-
continue native placement tracking under `failedFallback = NATIVE`.
|
|
599
|
-
|
|
600
|
-
## Maven publication
|
|
601
|
-
|
|
602
|
-
The `Native UI Engine Maven Release` workflow is manually dispatched on `main` with the exact engine package version. That commit must first pass `Native UI Engine` CI. It signs an AAR, source archive, generated Kotlin API documentation and Maven metadata, uploads once, and completes when Central reports `PUBLISHING` or `PUBLISHED`. The deployment ID and state changes are recorded in the run summary. `PUBLISHING` means validation passed and Central accepted publication; it does not guarantee that artifacts can already be downloaded. This workflow does not wait for distribution. The npm release still requires the matching POM and AAR to be available on Maven Central. Existing coordinates are never overwritten; if an upload times out, inspect the recorded deployment in Central before taking further action.
|
|
603
|
-
|
|
604
|
-
Configure the repository's Actions secrets with `CENTRAL_USERNAME`, `CENTRAL_PASSWORD` (a Central user token), `MAVEN_SIGNING_KEY` (ASCII-armored private key) and `MAVEN_SIGNING_PASSWORD`. The signing public key must be available to Central. These are the same credential types used by cf-workers-native; GitHub secrets are scoped to their repository and must be provisioned here separately.
|
|
605
|
-
|
|
606
|
-
For local packaging verification, run `python3 scripts/native-ui-engine-release/verify.py` with JDK 21, Android SDK 36 and GnuPG. It creates a disposable key, validates signatures and checksums, and deletes its test ZIP without uploading. Publication always rebuilds from clean outputs with the real signing key. Publish its Maven artifact before the corresponding npm release, including prereleases.
|
|
607
|
-
|
|
608
|
-
## Maintainer release setup
|
|
609
|
-
|
|
610
|
-
This repository starts from the merged Engine implementation at [ionic-theme-ios27 `b218d8b`](https://github.com/rdlabo-dev/ionic-theme-ios27/commit/b218d8bfe0c87c02addc668fee9c7aa333489486). Extraction preserves the merged implementation; Cordova host support is developed in this repository. The renamed package starts at version `0.0.1`. Publish the same version for npm, SwiftPM and Maven; do not reuse the old package coordinates.
|
|
611
|
-
|
|
612
|
-
Before the first release, create the npm organization `webview-native-ui` and verify the Maven Central namespace `io.github.webview-native-ui`. Configure npm trusted publishing for `webview-native-ui/engine`, workflow `release.yml`, and provision the Maven secrets above in this repository. Credentials are not copied by moving source files.
|
|
613
|
-
|
|
614
|
-
1. Prepare the next version and release notes on `main`; use `npm run release` to create a standard `vX.Y.Z` tag/GitHub release. Keep the GitHub release as a draft until Maven is downloadable.
|
|
615
|
-
2. After that main commit passes CI, dispatch `native-ui-engine-release.yml` with the exact package version.
|
|
616
|
-
3. Central `PUBLISHING` is an accepted upload, not download readiness. Once the POM and AAR are available, publish the prepared GitHub release. `release.yml` verifies the tag, package version, CI and Maven artifacts before npm publication (`next` for prereleases, `latest` otherwise).
|
|
617
|
-
|
|
618
|
-
No build or test command publishes a package. Existing npm/Maven versions are never overwritten.
|
|
47
|
+
- [Projection behavior and advanced integration](docs/projection.md) — DOM requirements, lifecycle, fallback policies, optional Ionic support and lower-level APIs.
|
|
48
|
+
- [Development and releases](docs/maintenance.md) — repository structure, validation and publishing.
|
|
619
49
|
|
|
620
50
|
## License
|
|
621
51
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
[NOTICE](NOTICE) preserves the upstream MIT notice for code adapted from ionic-theme-ios27 / capacitor-native-ui. Previously granted MIT permissions remain applicable to those versions and portions. The theme and Native UI plugin remain MIT; the engine's Zlib license does not change their licenses or the licenses of applications that depend on it. The npm archive and Android artifacts include the engine notices; SwiftPM users obtain them in this repository alongside the engine sources.
|
|
52
|
+
[MIT](LICENSE). Commercial use, modification and redistribution are permitted with the license's notice and attribution requirements. The Engine's license does not change the licenses of applications or plugins that depend on it.
|