@webview-native-ui/engine 0.0.5 → 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.
- package/README.md +19 -12
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/native/session.d.ts +5 -0
- package/dist/native/session.js +83 -14
- package/dist/native/updates.d.ts +16 -0
- package/dist/native/updates.js +108 -0
- package/dist/native/values.d.ts +5 -0
- package/dist/native/values.js +27 -0
- package/dist/projection/surface.d.ts +35 -0
- package/dist/projection/surface.js +81 -0
- package/dist/projection/surfaces.d.ts +20 -0
- package/dist/projection/surfaces.js +116 -0
- package/dist/projection/web.js +22 -12
- package/dist/types.d.ts +4 -0
- package/docs/README.md +70 -0
- package/docs/advanced/README.md +20 -0
- package/docs/advanced/android-placement.md +48 -0
- package/docs/advanced/native-handoff.md +89 -0
- package/docs/advanced/renderer.md +148 -0
- package/docs/advanced/update-queue.md +58 -0
- package/docs/advanced/web-projection.md +88 -0
- package/docs/assets/ios-native-capture-comparison.png +0 -0
- package/docs/concepts.md +97 -0
- package/docs/configuration/README.md +29 -0
- package/docs/configuration/capture.md +27 -0
- package/docs/configuration/events.md +35 -0
- package/docs/configuration/ionic-overlays.md +65 -0
- package/docs/configuration/values.md +125 -0
- package/docs/cordova.md +1 -1
- package/docs/{capacitor-plugin.md → getting-started/capacitor.md} +45 -47
- package/docs/{cordova-plugin.md → getting-started/cordova.md} +32 -15
- package/docs/getting-started/initialization.md +79 -0
- package/docs/maintenance.md +14 -2
- package/docs/{projection-containers.md → reference/containers.md} +6 -2
- package/docs/reference/platforms.md +137 -0
- package/docs/reference/projection.md +134 -0
- package/docs/reference/state.md +39 -0
- package/ios/Sources/WebViewNativeUIEngine/ClippedView.swift +12 -12
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +70 -9
- package/ios/Sources/WebViewNativeUIEngine/Frame.swift +10 -10
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +92 -44
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -3
- package/ios/Sources/WebViewNativeUIEngine/ScrollBindings.swift +3 -0
- package/ios/Sources/WebViewNativeUIEngine/ScrollTracker.swift +10 -8
- package/package.json +10 -3
- package/docs/projection.md +0 -364
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Projection reference
|
|
2
|
+
|
|
3
|
+
Use this reference to check integration contracts and platform limits. Start with
|
|
4
|
+
[concepts and integration choices](../concepts.md) to decide whether the Engine fits
|
|
5
|
+
your UI. Use [native initialization](../getting-started/initialization.md) to distinguish required
|
|
6
|
+
inputs, optional settings and advanced APIs, then follow the [Capacitor](../getting-started/capacitor.md),
|
|
7
|
+
[Cordova](../getting-started/cordova.md) or [renderer](../advanced/renderer.md) guide.
|
|
8
|
+
|
|
9
|
+
This page covers DOM lifetime, destination precedence, host requirements and native
|
|
10
|
+
eligibility. See [platform limits](platforms.md) for capture details, [projection state](state.md)
|
|
11
|
+
for the intent attribute, and [container support](containers.md) for HTML/Ionic hosts.
|
|
12
|
+
|
|
13
|
+
Use this contract when deciding what must survive a transition. For example, a
|
|
14
|
+
modal closes while a native restore reply is still in flight, and the framework
|
|
15
|
+
replaces the page DOM. The old reply must not reveal the departed page's controls;
|
|
16
|
+
the lifetime and destination rules below define that boundary.
|
|
17
|
+
|
|
18
|
+
## DOM and page lifetime
|
|
19
|
+
|
|
20
|
+
Native registration is independent of the JavaScript framework. Its selector identifies
|
|
21
|
+
one fallback container in the current document; it may appear after registration.
|
|
22
|
+
When a framework removes or replaces that container, the Engine retires the old
|
|
23
|
+
projection and binds the current matching element. A pending drag or native draw
|
|
24
|
+
acknowledgement from the departed container must not hold up its replacement.
|
|
25
|
+
Moving the same element between parents refreshes its scroll bindings without
|
|
26
|
+
requiring the plugin to register it again.
|
|
27
|
+
|
|
28
|
+
A full HTML navigation creates a new document and runtime session. The native
|
|
29
|
+
registration installs the runtime in the new document and retains the plugin's
|
|
30
|
+
explicit projection destination; images and replies belong to their original
|
|
31
|
+
document. Keep registrations for the native renderer's lifetime, rather than
|
|
32
|
+
recreating them for each framework page event.
|
|
33
|
+
|
|
34
|
+
These rules apply to selector-based native registration. A lower-level
|
|
35
|
+
`createWebProjectionScope(element)` intentionally binds that particular element;
|
|
36
|
+
its renderer remains responsible for replacing the scope when it changes elements.
|
|
37
|
+
|
|
38
|
+
The Engine cannot infer every framework's transition or modal semantics from DOM
|
|
39
|
+
mutations. Use the existing projection notifications for those transitions, and an
|
|
40
|
+
explicit overlay integration when needed. Ionic support is opt-in; it is not required
|
|
41
|
+
for ordinary DOM lifetime handling. Framework-specific events are not installed
|
|
42
|
+
automatically. Native view creation, layout constraints and SDK
|
|
43
|
+
lifecycle remain owned by the plugin.
|
|
44
|
+
|
|
45
|
+
## Projection contract
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
| Intended destination | Required behavior |
|
|
50
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `native` | Show the interactive native view at confirmed geometry. During motion, use supported native tracking or a prepared image. |
|
|
52
|
+
| `web` | Keep native content below the covering Web layer by removing its native projection; use a prepared image or the plugin's Web fallback. |
|
|
53
|
+
| `none` | Retire native content, projected images and fallback content. Later update/capture callbacks must not show it again. |
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
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. See [renderer DOM integration](../advanced/web-projection.md) for excluding Engine-owned mutations from renderer invalidation. This helper performs a style read and can trigger attribute observers; it does not eliminate measurement or transport costs.
|
|
62
|
+
|
|
63
|
+
## DOM and native surface requirements
|
|
64
|
+
|
|
65
|
+
`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.
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
See [Projection container support](containers.md) for the HTML and complete Ionic element tables, sizing conditions and a wrapper example.
|
|
72
|
+
|
|
73
|
+
### Native surface requirements
|
|
74
|
+
|
|
75
|
+
When a plugin displays several independently moving native views—for example,
|
|
76
|
+
several advertisements in a scrolling feed—create a separate registration for
|
|
77
|
+
each view and give each one a selector matching its own DOM container. This lets
|
|
78
|
+
one view move, hide or be replaced without changing another view's projection.
|
|
79
|
+
|
|
80
|
+
If those registrations share plugin notifications, use `receiveListeners.match`
|
|
81
|
+
to route each notification to its intended registration. The matching fields are
|
|
82
|
+
chosen by your plugin, not reserved Engine identifiers. For example, include
|
|
83
|
+
`viewKey` to identify the displayed item and `instanceId` to distinguish successive
|
|
84
|
+
loaded instances for that item:
|
|
85
|
+
|
|
86
|
+
```swift
|
|
87
|
+
match: ["viewKey": viewKey, "instanceId": instanceId]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```kotlin
|
|
91
|
+
match = mapOf("viewKey" to viewKey, "instanceId" to instanceId)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Include the same fields in the notification payload. Dispose the old registration
|
|
95
|
+
when replacing its loaded content, even if the native container is reused, so
|
|
96
|
+
notifications for the previous instance cannot affect the replacement.
|
|
97
|
+
|
|
98
|
+
For third-party SDK views, keep loading and interaction handling in your plugin.
|
|
99
|
+
Use `canCapture` to report whether the current content may be captured under the
|
|
100
|
+
SDK's rules and your integration's supported capture conditions. For example, an
|
|
101
|
+
advertising SDK may require different handling for video and static content.
|
|
102
|
+
The Engine does not grant permission to capture SDK content or replace its
|
|
103
|
+
click and impression handling.
|
|
104
|
+
|
|
105
|
+
For SDK consumers without an equivalent Web fallback, set `failedFallback: .native`
|
|
106
|
+
on iOS or `failedFallback = NativeUIProjection.FailedFallback.NATIVE` on Android.
|
|
107
|
+
The default is `.web` / `WEB`, preserving existing consumers. If automatic motion
|
|
108
|
+
cannot use a prepared image (not ready, unsupported capture, or rejected handoff),
|
|
109
|
+
`native` preserves the live native view and keeps fallback DOM hidden. The consumer
|
|
110
|
+
must continue positioning that view; this option does not create native scroll
|
|
111
|
+
tracking on Android. Explicit `web`, `none`, and overlay coverage take precedence:
|
|
112
|
+
this option never raises a native view above an intentionally covering Web layer.
|
|
113
|
+
|
|
114
|
+
On Android, consumers whose asynchronous hide method promises that all pixels have
|
|
115
|
+
disappeared must call `projection.hide { completed -> ... }` and resolve only when
|
|
116
|
+
`completed` is true. Native handoff copies disappear immediately; completion waits
|
|
117
|
+
for synchronous Web-proxy retirement. A timeout reports failure, not success.
|
|
118
|
+
Call `projection.show()` when presenting again, in addition to normal plugin events;
|
|
119
|
+
this also preserves presentation intent if event subscription is still starting.
|
|
120
|
+
|
|
121
|
+
Scroll observation follows composed-tree ancestors, including assigned slots.
|
|
122
|
+
Non-composed scroll events inside an Ionic shadow tree are observed at their source;
|
|
123
|
+
applications do not add an extra listener or dispatch synthetic document events.
|
|
124
|
+
Listeners follow host reparenting and slot reassignment and are removed on teardown.
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
130
|
+
For capture modes, translucent surfaces, image color and private storage, see
|
|
131
|
+
[capture and platform limits](platforms.md). Custom renderer bridge and scroll
|
|
132
|
+
bindings are documented under [native handoff](../advanced/native-handoff.md).
|
|
133
|
+
|
|
134
|
+
[Documentation overview](../README.md)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Observe projection state
|
|
2
|
+
|
|
3
|
+
Projection state answers **where the component should be presented**. It is
|
|
4
|
+
independent of its value, such as a toggle's checked state or a selected tab.
|
|
5
|
+
See [value bindings](../configuration/values.md) for that connection.
|
|
6
|
+
|
|
7
|
+
For example, a tab bar unexpectedly remains hidden after dismissing a modal.
|
|
8
|
+
Inspect this attribute to distinguish a remaining `web` request from a `native`
|
|
9
|
+
request whose restoration is still pending. It helps diagnose the ownership
|
|
10
|
+
choice; it is not permission to reveal the bar or discard its image early.
|
|
11
|
+
|
|
12
|
+
A selector-based `NativeUIProjection` registration writes the read-only
|
|
13
|
+
`data-native-ui-destination` attribute on its current DOM container:
|
|
14
|
+
|
|
15
|
+
| Value | Meaning |
|
|
16
|
+
| -------- | ----------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `native` | Native presentation is requested. |
|
|
18
|
+
| `web` | Web presentation is requested, explicitly or because a registered overlay covers the container. |
|
|
19
|
+
| `none` | No presentation is requested. |
|
|
20
|
+
|
|
21
|
+
Without a prior destination notification, startup derives the initial intent from
|
|
22
|
+
native view visibility; a hidden or absent view starts as `none`. The plugin's
|
|
23
|
+
projection events then select `native`, `web` or `none`. Overlay coverage
|
|
24
|
+
changes an otherwise native request to Web; it does not override an explicit
|
|
25
|
+
`none`. The Engine updates the attribute as it reconciles presentation and removes
|
|
26
|
+
it when the container is replaced or the registration is destroyed. Reserve this
|
|
27
|
+
attribute for the Engine rather than setting it from application code.
|
|
28
|
+
|
|
29
|
+
This is presentation **intent**, not an acknowledgement that a native frame or
|
|
30
|
+
image has finished drawing. Web presentation may use a cached native image or the
|
|
31
|
+
existing interactive fallback according to availability and the integration's
|
|
32
|
+
policy. Do not hide the fallback solely because this attribute says `native`:
|
|
33
|
+
the Engine's handoff keeps the previous representation until the next one is ready.
|
|
34
|
+
|
|
35
|
+
Use this attribute for diagnostics and application state that does not take over
|
|
36
|
+
visibility. Continue using the documented projection events to request a change.
|
|
37
|
+
See the [projection contract](projection.md#projection-contract) for ownership,
|
|
38
|
+
fallback and interaction rules. Internal snapshot attributes are not additional
|
|
39
|
+
public state APIs.
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
/// Keeps native content at full size while clipping hit testing and visible pixels.
|
|
3
3
|
public final class NativeUIClippedView: UIView {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
public let content: UIView
|
|
5
|
+
public init(content: UIView) { self.content = content; super.init(frame: .zero); addSubview(content) }
|
|
6
|
+
@available(*, unavailable) required init?(coder: NSCoder) { fatalError() }
|
|
7
|
+
public func place(_ frame: CGRect, clip: CGRect? = nil) {
|
|
8
|
+
clipsToBounds = clip != nil
|
|
9
|
+
let visible = clip.map { frame.intersection($0) } ?? frame
|
|
10
|
+
isHidden = visible.isNull || visible.isEmpty
|
|
11
|
+
guard !isHidden else { return }
|
|
12
|
+
if self.frame != visible { self.frame = visible }
|
|
13
|
+
let local = CGRect(x: frame.minX - visible.minX, y: frame.minY - visible.minY, width: frame.width, height: frame.height)
|
|
14
|
+
if content.frame != local { content.frame = local }
|
|
15
|
+
}
|
|
16
16
|
}
|
|
@@ -27,6 +27,66 @@ public final class NativeUICoveredSnapshots {
|
|
|
27
27
|
private var observer: SnapshotTouchObserver?
|
|
28
28
|
private let directory = FileManager.default.temporaryDirectory.appendingPathComponent("native-ui-snapshots-\(UUID().uuidString)")
|
|
29
29
|
public private(set) var coveringToken: String?
|
|
30
|
+
/// Dispatch the renderer's existing ordered image-handoff transport on the main thread.
|
|
31
|
+
/// The renderer supplies eligible native bounds and the complete views that may be hidden.
|
|
32
|
+
public func handle(_ request: [String: Any], in webView: UIView,
|
|
33
|
+
frames: @escaping (Set<String>, Int) -> [String: CGRect],
|
|
34
|
+
views: (Set<String>) -> [UIView]?, complete: @escaping ([String: Any]) -> Void) {
|
|
35
|
+
let token = request["token"] as? String ?? ""
|
|
36
|
+
switch request["action"] as? String {
|
|
37
|
+
case "prepare":
|
|
38
|
+
guard let ids = request["ids"] as? [String], !ids.isEmpty, !ids.contains(""), Set(ids).count == ids.count,
|
|
39
|
+
let revision = request["revision"] as? Int,
|
|
40
|
+
let width = request["viewportWidth"] as? Double, width.isFinite, width > 0 else { complete([:]); return }
|
|
41
|
+
let expected = Set(ids)
|
|
42
|
+
let firstFrames = frames(expected, revision)
|
|
43
|
+
guard Set(firstFrames.keys) == expected else { complete([:]); return }
|
|
44
|
+
var initial: [String: CGRect]? = firstFrames
|
|
45
|
+
prepare(frames: {
|
|
46
|
+
let measured = initial ?? frames(expected, revision)
|
|
47
|
+
initial = nil
|
|
48
|
+
return Set(measured.keys) == expected ? measured : [:]
|
|
49
|
+
}, in: webView, viewportWidth: width, complete: complete)
|
|
50
|
+
case "cover":
|
|
51
|
+
let ids = identifiers(for: token)
|
|
52
|
+
let destination = request["parts"] == nil ? nil : projectionFrames(request, in: webView)
|
|
53
|
+
guard !ids.isEmpty, let selected = views(ids), !selected.isEmpty,
|
|
54
|
+
request["parts"] == nil || destination != nil else { complete(["shown": false]); return }
|
|
55
|
+
var seen = Set<ObjectIdentifier>()
|
|
56
|
+
complete(["shown": cover(token, in: webView, views: selected.filter { seen.insert(ObjectIdentifier($0)).inserted },
|
|
57
|
+
transient: request["transient"] as? Bool == true, frames: destination)])
|
|
58
|
+
case "restore":
|
|
59
|
+
guard let destination = projectionFrames(request, in: webView) else { complete(["shown": false]); return }
|
|
60
|
+
complete(["shown": restore(in: webView, token: token, frames: destination)])
|
|
61
|
+
case "painted", "restored":
|
|
62
|
+
retireCopy(token, entry: request["action"] as? String == "painted")
|
|
63
|
+
complete([:])
|
|
64
|
+
case "release": release(token); complete([:])
|
|
65
|
+
case "reset": reset(); complete([:])
|
|
66
|
+
default: complete([:])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Convert Web viewport CSS pixels to native points, rejecting partial or malformed destinations.
|
|
71
|
+
func projectionFrames(_ request: [String: Any], in webView: UIView) -> [String: CGRect]? {
|
|
72
|
+
guard let token = request["token"] as? String,
|
|
73
|
+
let width = request["viewportWidth"] as? Double, width.isFinite, width > 0,
|
|
74
|
+
webView.bounds.width > 0, let parts = request["parts"] as? [[String: Any]], !parts.isEmpty else { return nil }
|
|
75
|
+
let scale = webView.bounds.width / width
|
|
76
|
+
var result: [String: CGRect] = [:]
|
|
77
|
+
for part in parts {
|
|
78
|
+
guard let ids = part["ids"] as? [String], !ids.isEmpty,
|
|
79
|
+
let x = part["x"] as? Double, let y = part["y"] as? Double,
|
|
80
|
+
let w = part["width"] as? Double, let h = part["height"] as? Double,
|
|
81
|
+
[x, y, w, h, x * scale, y * scale, w * scale, h * scale].allSatisfy({ $0.isFinite }), w > 0, h > 0 else { return nil }
|
|
82
|
+
for id in ids {
|
|
83
|
+
guard !id.isEmpty, result[id] == nil else { return nil }
|
|
84
|
+
result[id] = CGRect(x: x * scale, y: y * scale, width: w * scale, height: h * scale)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return !result.isEmpty && Set(result.keys) == identifiers(for: token) ? result : nil
|
|
88
|
+
}
|
|
89
|
+
|
|
30
90
|
public var covering: Bool { coveringToken != nil }
|
|
31
91
|
|
|
32
92
|
public func observe(_ window: UIWindow, changed: @escaping (Bool) -> Void) {
|
|
@@ -46,7 +106,7 @@ public final class NativeUICoveredSnapshots {
|
|
|
46
106
|
// Gesture release can keep changing SwiftUI button bounds after touchesEnded.
|
|
47
107
|
// Wait off the presentation path for unchanged rendered geometry, not a guessed delay.
|
|
48
108
|
public func prepare(frames: @escaping () -> [String: CGRect], in webView: UIView, viewportWidth: CGFloat,
|
|
49
|
-
|
|
109
|
+
complete: @escaping ([String: Any]) -> Void) {
|
|
50
110
|
let stamp = generation
|
|
51
111
|
let start = CACurrentMediaTime()
|
|
52
112
|
func sample(_ previous: [String: CGRect], _ stable: Int) {
|
|
@@ -63,7 +123,7 @@ public final class NativeUICoveredSnapshots {
|
|
|
63
123
|
}
|
|
64
124
|
|
|
65
125
|
private func capture(frames: [String: CGRect], in webView: UIView, viewportWidth: CGFloat,
|
|
66
|
-
|
|
126
|
+
complete: @escaping ([String: Any]) -> Void) {
|
|
67
127
|
guard !touching, let window = webView.window, !frames.isEmpty,
|
|
68
128
|
viewportWidth > 0, webView.bounds.width > 0 else { complete([:]); return }
|
|
69
129
|
let stamp = generation
|
|
@@ -88,8 +148,9 @@ public final class NativeUICoveredSnapshots {
|
|
|
88
148
|
guard let space = CGColorSpace(name: CGColorSpace.extendedDisplayP3),
|
|
89
149
|
let outputSpace = CGColorSpace(name: CGColorSpace.displayP3),
|
|
90
150
|
let context = CGContext(data: nil, width: Int(bounds.width * scale), height: Int(bounds.height * scale),
|
|
91
|
-
|
|
92
|
-
|
|
151
|
+
bitsPerComponent: 16, bytesPerRow: 0, space: space,
|
|
152
|
+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue |
|
|
153
|
+
CGBitmapInfo.floatComponents.rawValue | CGBitmapInfo.byteOrder16Little.rawValue)
|
|
93
154
|
else { complete([:]); return }
|
|
94
155
|
context.translateBy(x: 0, y: bounds.height * scale)
|
|
95
156
|
context.scaleBy(x: scale, y: -scale)
|
|
@@ -116,12 +177,12 @@ public final class NativeUICoveredSnapshots {
|
|
|
116
177
|
for (index, crop) in crops.enumerated() {
|
|
117
178
|
guard let pixels = crop.image.cgImage,
|
|
118
179
|
let png = imageContext.pngRepresentation(of: CIImage(cgImage: pixels), format: .RGBA16,
|
|
119
|
-
|
|
180
|
+
colorSpace: outputSpace, options: [:]) else { throw CocoaError(.fileWriteUnknown) }
|
|
120
181
|
// Native entry/return copies must use the same encoded pixels as Web,
|
|
121
182
|
// including gamut clipping performed by integer PNG conversion.
|
|
122
183
|
guard let source = CGImageSourceCreateWithData(png as CFData, nil),
|
|
123
184
|
let decoded = CGImageSourceCreateImageAtIndex(source, 0,
|
|
124
|
-
|
|
185
|
+
[kCGImageSourceShouldCacheImmediately: true] as CFDictionary)
|
|
125
186
|
else { throw CocoaError(.fileWriteUnknown) }
|
|
126
187
|
let image = UIImage(cgImage: decoded, scale: scale, orientation: .up)
|
|
127
188
|
let file = directory.appendingPathComponent("\(token)-\(index).png")
|
|
@@ -150,8 +211,8 @@ public final class NativeUICoveredSnapshots {
|
|
|
150
211
|
|
|
151
212
|
/// Install prepared pixels before hiding live views, within the same main-thread turn.
|
|
152
213
|
/// A transient (moving) surface does not replace background covering ownership.
|
|
153
|
-
@discardableResult public func cover(_ token: String, in webView: UIView, views: [UIView], transient: Bool = false) -> Bool {
|
|
154
|
-
guard restore(in: webView, token: token, entry: true) else { return false }
|
|
214
|
+
@discardableResult public func cover(_ token: String, in webView: UIView, views: [UIView], transient: Bool = false, frames: [String: CGRect]? = nil) -> Bool {
|
|
215
|
+
guard restore(in: webView, token: token, frames: frames, entry: true) else { return false }
|
|
155
216
|
if !transient { coveringToken = token }
|
|
156
217
|
hide(views, for: token)
|
|
157
218
|
return true
|
|
@@ -238,7 +299,7 @@ public final class NativeUICoveredSnapshots {
|
|
|
238
299
|
}
|
|
239
300
|
}
|
|
240
301
|
|
|
241
|
-
|
|
302
|
+
// Observes the start of interaction without recognizing or delaying the app's gestures.
|
|
242
303
|
private final class SnapshotTouchObserver: UIGestureRecognizer {
|
|
243
304
|
private let changed: (Bool) -> Void
|
|
244
305
|
private var touches = Set<UITouch>()
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import CoreGraphics
|
|
2
2
|
public struct NativeUIFrame: Codable, Equatable {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
public let x: Double
|
|
4
|
+
public let y: Double
|
|
5
|
+
public let width: Double
|
|
6
|
+
public let height: Double
|
|
7
|
+
public init(x: Double, y: Double, width: Double, height: Double) { self.x = x; self.y = y; self.width = width; self.height = height }
|
|
8
|
+
public var rect: CGRect { CGRect(x: x, y: y, width: width, height: height) }
|
|
9
|
+
public var isValid: Bool { [x, y, width, height].allSatisfy(\.isFinite) && width > 0 && height > 0 }
|
|
10
10
|
}
|
|
11
11
|
public struct NativeUIScrollNode: Decodable {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
public let id: String
|
|
13
|
+
public let scroll: NativeUIScrollPlacement?
|
|
14
|
+
public init(id: String, scroll: NativeUIScrollPlacement?) { self.id = id; self.scroll = scroll }
|
|
15
15
|
}
|