@octane-xplat/macos-renderer 0.0.1 → 0.11.0
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 +21 -0
- package/README.md +232 -3
- package/package.json +38 -3
- package/src/appkit.d.ts +199 -0
- package/src/fonts.ts +100 -0
- package/src/image.ts +23 -0
- package/src/index.d.ts +18 -0
- package/src/index.ts +4654 -0
- package/src/intrinsics.d.ts +62 -0
- package/src/jsx-runtime.d.ts +7 -0
- package/src/layer.d.mts +17 -0
- package/src/layer.mjs +221 -0
- package/src/native-core-shim.ts +47 -0
- package/src/native-escape-props.ts +30 -0
- package/src/presentation.mjs +501 -0
- package/src/webview.ts +252 -0
package/src/webview.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/** Embedded document surface; deliberately has no app-shell service bridge. */
|
|
2
|
+
// The minimal AppKit host links AppKit, so load WebKit before resolving classes.
|
|
3
|
+
const webKit = dlopen('/System/Library/Frameworks/WebKit.framework/WebKit', 2)
|
|
4
|
+
if (!webKit) {
|
|
5
|
+
throw new Error('Unable to load the system WebKit framework')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface WebViewState {
|
|
9
|
+
props: Record<string, any>
|
|
10
|
+
generation: number
|
|
11
|
+
loaded: boolean
|
|
12
|
+
navigation: any
|
|
13
|
+
delegate: any
|
|
14
|
+
inline?: boolean
|
|
15
|
+
source?: any
|
|
16
|
+
emit(name: string, payload: any): void
|
|
17
|
+
measure(): void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const states = new WeakMap<object, WebViewState>()
|
|
21
|
+
|
|
22
|
+
const EmbeddedNavigationDelegate = NSObject.extend(
|
|
23
|
+
{
|
|
24
|
+
'webView:didStartProvisionalNavigation:'(view: WKWebView, navigation: any) {
|
|
25
|
+
const state = states.get(view)
|
|
26
|
+
if (!state) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
state.navigation = navigation
|
|
31
|
+
state.generation++
|
|
32
|
+
state.loaded = false
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
'webView:didFinishNavigation:'(view: WKWebView, navigation: any) {
|
|
36
|
+
const state = states.get(view)
|
|
37
|
+
if (!state || state.navigation !== navigation) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
state.loaded = true
|
|
42
|
+
state.measure()
|
|
43
|
+
state.emit('onLoad', { url: state.inline ? undefined : view.URL?.absoluteString })
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
'webView:didFailProvisionalNavigation:withError:'(
|
|
47
|
+
view: WKWebView,
|
|
48
|
+
navigation: any,
|
|
49
|
+
error: NSError,
|
|
50
|
+
) {
|
|
51
|
+
;(this as any)['webView:didFailNavigation:withError:'](view, navigation, error)
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
'webView:didFailNavigation:withError:'(view: WKWebView, navigation: any, error: NSError) {
|
|
55
|
+
const state = states.get(view)
|
|
56
|
+
if (!state || state.navigation !== navigation) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
state.loaded = false
|
|
61
|
+
state.emit('onError', {
|
|
62
|
+
url: state.inline
|
|
63
|
+
? undefined
|
|
64
|
+
: (error.userInfo?.objectForKey?.(NSURLErrorFailingURLErrorKey)?.absoluteString ??
|
|
65
|
+
state.props.src ??
|
|
66
|
+
view.URL?.absoluteString),
|
|
67
|
+
error: String(error.localizedDescription ?? error),
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
'webViewWebContentProcessDidTerminate:'(view: WKWebView) {
|
|
72
|
+
const state = states.get(view)
|
|
73
|
+
if (!state) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
state.loaded = false
|
|
78
|
+
state.generation++
|
|
79
|
+
state.emit('onError', {
|
|
80
|
+
url: state.inline ? undefined : view.URL?.absoluteString,
|
|
81
|
+
error: 'Web content process terminated',
|
|
82
|
+
})
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'XplatEmbeddedNavigationDelegate',
|
|
87
|
+
protocols: [WKNavigationDelegate],
|
|
88
|
+
exposedMethods: {
|
|
89
|
+
'webView:didStartProvisionalNavigation:': {
|
|
90
|
+
params: [WKWebView, NSObject],
|
|
91
|
+
returns: interop.types.void,
|
|
92
|
+
},
|
|
93
|
+
'webView:didFinishNavigation:': {
|
|
94
|
+
params: [WKWebView, NSObject],
|
|
95
|
+
returns: interop.types.void,
|
|
96
|
+
},
|
|
97
|
+
'webView:didFailProvisionalNavigation:withError:': {
|
|
98
|
+
params: [WKWebView, NSObject, NSError],
|
|
99
|
+
returns: interop.types.void,
|
|
100
|
+
},
|
|
101
|
+
'webView:didFailNavigation:withError:': {
|
|
102
|
+
params: [WKWebView, NSObject, NSError],
|
|
103
|
+
returns: interop.types.void,
|
|
104
|
+
},
|
|
105
|
+
'webViewWebContentProcessDidTerminate:': { params: [WKWebView], returns: interop.types.void },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const scrollScript = (enabled: any) => `(() => {
|
|
111
|
+
const id = '__octane_xplat_scroll';
|
|
112
|
+
document.getElementById(id)?.remove();
|
|
113
|
+
if (${enabled === false}) {
|
|
114
|
+
const style = document.createElement('style');
|
|
115
|
+
style.id = id;
|
|
116
|
+
style.textContent = 'html,body { overflow: hidden !important; }';
|
|
117
|
+
(document.head || document.documentElement).appendChild(style);
|
|
118
|
+
}
|
|
119
|
+
})()`
|
|
120
|
+
|
|
121
|
+
const measureScript = `(() => {
|
|
122
|
+
const body = document.body, root = document.documentElement;
|
|
123
|
+
if (!body || !root) return null;
|
|
124
|
+
return JSON.stringify({width: Math.max(body.scrollWidth, root.scrollWidth), height: Math.max(body.scrollHeight, root.scrollHeight)});
|
|
125
|
+
})()`
|
|
126
|
+
|
|
127
|
+
export function makeWebView(): WKWebView {
|
|
128
|
+
const view = WKWebView.alloc().initWithFrameConfiguration(
|
|
129
|
+
{ origin: { x: 0, y: 0 }, size: { width: 320, height: 150 } },
|
|
130
|
+
WKWebViewConfiguration.new(),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
134
|
+
return view
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function updateWebView(
|
|
138
|
+
node: import('./index').ElementNode,
|
|
139
|
+
changed: Record<string, any>,
|
|
140
|
+
resize: (height: number) => void,
|
|
141
|
+
) {
|
|
142
|
+
const view = node.view as WKWebView
|
|
143
|
+
let state = states.get(view)
|
|
144
|
+
if (!state) {
|
|
145
|
+
// `created` is a const so the closures below keep their declared type.
|
|
146
|
+
const created: WebViewState = {
|
|
147
|
+
props: node.props,
|
|
148
|
+
generation: 0,
|
|
149
|
+
loaded: false,
|
|
150
|
+
navigation: null,
|
|
151
|
+
delegate: EmbeddedNavigationDelegate.alloc().init(),
|
|
152
|
+
emit(name: string, payload: any) {
|
|
153
|
+
const handler = created.props[name]
|
|
154
|
+
if (handler) {
|
|
155
|
+
try {
|
|
156
|
+
node.container.root!.eventScope('discrete', () => handler(payload))
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('[macos-event] WebView handler failed', error)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
measure() {
|
|
163
|
+
const generation = created.generation
|
|
164
|
+
view.evaluateJavaScriptCompletionHandler(scrollScript(created.props.scrollEnabled), null)
|
|
165
|
+
if (!created.props.matchContents && !created.props.onLayoutContent) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
view.evaluateJavaScriptCompletionHandler(measureScript, (result: any, error: any) => {
|
|
170
|
+
if (
|
|
171
|
+
states.get(view) !== created ||
|
|
172
|
+
generation !== created.generation ||
|
|
173
|
+
error ||
|
|
174
|
+
!result
|
|
175
|
+
) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let size: any
|
|
180
|
+
try {
|
|
181
|
+
size = JSON.parse(String(result))
|
|
182
|
+
} catch {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
!size ||
|
|
188
|
+
!Number.isFinite(size.width) ||
|
|
189
|
+
!Number.isFinite(size.height) ||
|
|
190
|
+
size.width <= 0 ||
|
|
191
|
+
size.height <= 0
|
|
192
|
+
) {
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (created.props.matchContents) {
|
|
197
|
+
resize(size.height)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
created.emit('onLayoutContent', size)
|
|
201
|
+
})
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
states.set(view, created) // WKWebView holds its delegate weakly.
|
|
206
|
+
view.navigationDelegate = created.delegate
|
|
207
|
+
state = created
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
state.props = node.props
|
|
211
|
+
const inline = state.props.html !== undefined
|
|
212
|
+
const source = inline ? state.props.html : state.props.src
|
|
213
|
+
if (state.inline !== inline || state.source !== source) {
|
|
214
|
+
state.inline = inline
|
|
215
|
+
state.source = source
|
|
216
|
+
state.loaded = false
|
|
217
|
+
state.generation++
|
|
218
|
+
state.navigation = null
|
|
219
|
+
view.stopLoading()
|
|
220
|
+
if (inline || !source) {
|
|
221
|
+
state.navigation = view.loadHTMLStringBaseURL(String(source ?? ''), null)
|
|
222
|
+
} else {
|
|
223
|
+
const src = String(source)
|
|
224
|
+
const path = src.startsWith('~/')
|
|
225
|
+
? NSBundle.mainBundle.resourcePath + '/' + src.slice(2)
|
|
226
|
+
: src
|
|
227
|
+
|
|
228
|
+
const url = path.startsWith('/') ? NSURL.fileURLWithPath(path) : NSURL.URLWithString(path)
|
|
229
|
+
if (!url) {
|
|
230
|
+
state.emit('onError', { url: src, error: 'Invalid document URL' })
|
|
231
|
+
} else if (url.isFileURL) {
|
|
232
|
+
state.navigation = view.loadFileURLAllowingReadAccessToURL(
|
|
233
|
+
url,
|
|
234
|
+
url.URLByDeletingLastPathComponent,
|
|
235
|
+
)
|
|
236
|
+
} else {
|
|
237
|
+
state.navigation = view.loadRequest(NSURLRequest.requestWithURL(url))
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} else if (
|
|
241
|
+
state.loaded &&
|
|
242
|
+
['scrollEnabled', 'matchContents', 'onLayoutContent'].some((name) => name in changed)
|
|
243
|
+
) {
|
|
244
|
+
state.measure()
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function disposeWebView(view: WKWebView) {
|
|
249
|
+
states.delete(view)
|
|
250
|
+
view.navigationDelegate = null
|
|
251
|
+
view.stopLoading()
|
|
252
|
+
}
|