sparkbun 0.1.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/bin/sparkbun.cjs +18 -0
- package/dist-linux-arm64/bsdiff +0 -0
- package/dist-linux-arm64/bspatch +0 -0
- package/dist-linux-arm64/libElectrobunCore.so +0 -0
- package/dist-linux-arm64/libNativeWrapper.so +0 -0
- package/dist-linux-arm64/libasar.so +0 -0
- package/dist-linux-x64/bsdiff +0 -0
- package/dist-linux-x64/bspatch +0 -0
- package/dist-linux-x64/libElectrobunCore.so +0 -0
- package/dist-linux-x64/libNativeWrapper.so +0 -0
- package/dist-linux-x64/libasar.so +0 -0
- package/dist-macos-arm64/bsdiff +0 -0
- package/dist-macos-arm64/bspatch +0 -0
- package/dist-macos-arm64/libElectrobunCore.dylib +0 -0
- package/dist-macos-arm64/libNativeWrapper.dylib +0 -0
- package/dist-macos-arm64/libasar.dylib +0 -0
- package/dist-macos-arm64/libwebgpu_dawn.dylib +0 -0
- package/dist-macos-arm64/preload-full.js +885 -0
- package/dist-macos-arm64/preload-sandboxed.js +111 -0
- package/dist-macos-arm64/process_helper +0 -0
- package/dist-win-x64/ElectrobunCore.dll +0 -0
- package/dist-win-x64/WebView2Loader.dll +0 -0
- package/dist-win-x64/bsdiff.exe +0 -0
- package/dist-win-x64/bspatch.exe +0 -0
- package/dist-win-x64/libNativeWrapper.dll +0 -0
- package/dist-win-x64/zig-asar/arm64/libasar.dll +0 -0
- package/dist-win-x64/zig-asar/x64/libasar.dll +0 -0
- package/package.json +47 -0
- package/scripts/build-and-upload-artifacts.js +207 -0
- package/scripts/gen-webgpu-ffi.mjs +162 -0
- package/scripts/install-windows-deps.ps1 +80 -0
- package/scripts/package-release.js +237 -0
- package/scripts/push-version.js +84 -0
- package/scripts/update-bun-version.ts +122 -0
- package/scripts/update-cef-version.ts +145 -0
- package/src/browser/builtinrpcSchema.ts +19 -0
- package/src/browser/global.d.ts +36 -0
- package/src/browser/index.ts +234 -0
- package/src/browser/webviewtag.ts +88 -0
- package/src/browser/wgputag.ts +48 -0
- package/src/bun/SparkBunConfig.ts +497 -0
- package/src/bun/__tests__/ffi-contract.test.ts +105 -0
- package/src/bun/core/ApplicationMenu.ts +70 -0
- package/src/bun/core/BrowserView.ts +416 -0
- package/src/bun/core/BrowserWindow.ts +396 -0
- package/src/bun/core/BuildConfig.ts +71 -0
- package/src/bun/core/ContextMenu.ts +75 -0
- package/src/bun/core/GpuWindow.ts +289 -0
- package/src/bun/core/Paths.ts +5 -0
- package/src/bun/core/Socket.ts +22 -0
- package/src/bun/core/Tray.ts +197 -0
- package/src/bun/core/Updater.ts +1131 -0
- package/src/bun/core/Utils.ts +487 -0
- package/src/bun/core/WGPUView.ts +167 -0
- package/src/bun/core/menuRoles.ts +181 -0
- package/src/bun/events/ApplicationEvents.ts +22 -0
- package/src/bun/events/event.ts +27 -0
- package/src/bun/events/eventEmitter.ts +45 -0
- package/src/bun/events/trayEvents.ts +11 -0
- package/src/bun/events/webviewEvents.ts +39 -0
- package/src/bun/events/windowEvents.ts +23 -0
- package/src/bun/index.ts +120 -0
- package/src/bun/preload/.generated/compiled.ts +2 -0
- package/src/bun/preload/build.ts +65 -0
- package/src/bun/preload/dragRegions.ts +41 -0
- package/src/bun/preload/encryption.ts +86 -0
- package/src/bun/preload/events.ts +171 -0
- package/src/bun/preload/globals.d.ts +45 -0
- package/src/bun/preload/index-sandboxed.ts +28 -0
- package/src/bun/preload/index.ts +77 -0
- package/src/bun/preload/internalRpc.ts +80 -0
- package/src/bun/preload/overlaySync.ts +107 -0
- package/src/bun/preload/webviewTag.ts +451 -0
- package/src/bun/preload/wgpuTag.ts +246 -0
- package/src/bun/proc/linux.md +43 -0
- package/src/bun/proc/native.ts +3253 -0
- package/src/bun/webGPU.ts +346 -0
- package/src/bun/webgpuAdapter.ts +3011 -0
- package/src/cli/bun.lockb +0 -0
- package/src/cli/index.ts +4653 -0
- package/src/cli/package-lock.json +81 -0
- package/src/cli/package.json +11 -0
- package/src/cli/templates/embedded.ts +2 -0
- package/src/core/build.zig +16 -0
- package/src/core/main.zig +3378 -0
- package/src/extractor/build.zig +22 -0
- package/src/installer/installer-template.ts +216 -0
- package/src/launcher/main.ts +221 -0
- package/src/native/build/libNativeWrapper.so +0 -0
- package/src/native/linux/build/nativeWrapper.o +0 -0
- package/src/native/linux/cef_loader.cpp +110 -0
- package/src/native/linux/cef_loader.h +28 -0
- package/src/native/linux/cef_process_helper_linux.cpp +160 -0
- package/src/native/linux/nativeWrapper.cpp +11768 -0
- package/src/native/macos/cef_process_helper_mac.cc +160 -0
- package/src/native/macos/nativeWrapper.mm +9172 -0
- package/src/native/shared/accelerator_parser.h +72 -0
- package/src/native/shared/app_paths.h +110 -0
- package/src/native/shared/asar.h +35 -0
- package/src/native/shared/cache_migration.h +244 -0
- package/src/native/shared/callbacks.h +57 -0
- package/src/native/shared/cef_response_filter.h +189 -0
- package/src/native/shared/chromium_flags.h +181 -0
- package/src/native/shared/config.h +66 -0
- package/src/native/shared/download_event.h +197 -0
- package/src/native/shared/ffi_helpers.h +139 -0
- package/src/native/shared/glob_match.h +59 -0
- package/src/native/shared/json_menu_parser.h +223 -0
- package/src/native/shared/mime_types.h +101 -0
- package/src/native/shared/navigation_rules.h +98 -0
- package/src/native/shared/partition_context.h +137 -0
- package/src/native/shared/pending_resize_queue.h +45 -0
- package/src/native/shared/permissions.h +118 -0
- package/src/native/shared/permissions_cef.h +74 -0
- package/src/native/shared/preload_script.h +71 -0
- package/src/native/shared/shutdown_guard.h +134 -0
- package/src/native/shared/thread_safe_map.h +138 -0
- package/src/native/shared/webview_storage.h +91 -0
- package/src/native/win/cef_process_helper_win.cpp +143 -0
- package/src/native/win/dcomp_compositor.h +352 -0
- package/src/native/win/nativeWrapper.cpp +12434 -0
- package/src/npmbin/index.js +34 -0
- package/src/shared/bun-version.ts +3 -0
- package/src/shared/cef-version.ts +5 -0
- package/src/shared/naming.test.ts +327 -0
- package/src/shared/naming.ts +188 -0
- package/src/shared/platform.ts +48 -0
- package/src/shared/rpc.ts +541 -0
- package/src/shared/sparkbun-version.ts +2 -0
- package/src/types/three.d.ts +1 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// <sparkbun-webview> Custom Element
|
|
2
|
+
// Provides OOPIF (out-of-process iframe) functionality
|
|
3
|
+
|
|
4
|
+
import "./globals.d.ts";
|
|
5
|
+
import { send, request } from "./internalRpc";
|
|
6
|
+
import { OverlaySyncController, type Rect } from "./overlaySync";
|
|
7
|
+
|
|
8
|
+
type WebviewEventType =
|
|
9
|
+
| "will-navigate"
|
|
10
|
+
| "did-navigate"
|
|
11
|
+
| "did-navigate-in-page"
|
|
12
|
+
| "did-commit-navigation"
|
|
13
|
+
| "dom-ready"
|
|
14
|
+
| "new-window-open"
|
|
15
|
+
| "host-message"
|
|
16
|
+
| "download-started"
|
|
17
|
+
| "download-progress"
|
|
18
|
+
| "download-completed"
|
|
19
|
+
| "download-failed"
|
|
20
|
+
| "load-started"
|
|
21
|
+
| "load-committed"
|
|
22
|
+
| "load-finished";
|
|
23
|
+
|
|
24
|
+
// Registry for webview instances (for event routing from bun)
|
|
25
|
+
export const webviewRegistry: Record<number, SparkBunWebviewTag> = {};
|
|
26
|
+
|
|
27
|
+
export class SparkBunWebviewTag extends HTMLElement {
|
|
28
|
+
webviewId: number | null = null;
|
|
29
|
+
maskSelectors: Set<string> = new Set();
|
|
30
|
+
private _sync: OverlaySyncController | null = null;
|
|
31
|
+
transparent = false;
|
|
32
|
+
passthroughEnabled = false;
|
|
33
|
+
hidden = false;
|
|
34
|
+
// Sandbox mode: when true, disables RPC and only allows event emission in the child webview
|
|
35
|
+
sandboxed = false;
|
|
36
|
+
private _eventListeners: Record<string, Array<(event: CustomEvent) => void>> =
|
|
37
|
+
{};
|
|
38
|
+
|
|
39
|
+
static get observedAttributes() {
|
|
40
|
+
return ["src", "html"];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
super();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
connectedCallback() {
|
|
48
|
+
requestAnimationFrame(() => this.initWebview());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
attributeChangedCallback(
|
|
52
|
+
name: string,
|
|
53
|
+
oldValue: string | null,
|
|
54
|
+
newValue: string | null,
|
|
55
|
+
) {
|
|
56
|
+
if (oldValue === newValue) return;
|
|
57
|
+
if (newValue === null) return;
|
|
58
|
+
if (this.webviewId === null) return;
|
|
59
|
+
if (name === "src") this.loadURL(newValue);
|
|
60
|
+
else if (name === "html") this.loadHTML(newValue);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
disconnectedCallback() {
|
|
64
|
+
if (this.webviewId !== null) {
|
|
65
|
+
send("webviewTagRemove", { id: this.webviewId });
|
|
66
|
+
delete webviewRegistry[this.webviewId];
|
|
67
|
+
}
|
|
68
|
+
if (this._sync) this._sync.stop();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private getInitialNavigationRules(): string[] | null {
|
|
72
|
+
const rawRules = this.getAttribute("navigation-rules");
|
|
73
|
+
if (rawRules === null) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const trimmed = rawRules.trim();
|
|
78
|
+
if (!trimmed) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(trimmed);
|
|
84
|
+
if (!Array.isArray(parsed) || !parsed.every((rule) => typeof rule === "string")) {
|
|
85
|
+
throw new Error("navigation-rules must be a JSON string array");
|
|
86
|
+
}
|
|
87
|
+
return parsed;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("Invalid navigation-rules attribute:", error);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async initWebview() {
|
|
95
|
+
const rect = this.getBoundingClientRect();
|
|
96
|
+
const initialRect = {
|
|
97
|
+
x: rect.x,
|
|
98
|
+
y: rect.y,
|
|
99
|
+
width: rect.width,
|
|
100
|
+
height: rect.height,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const url = this.getAttribute("src");
|
|
104
|
+
const html = this.getAttribute("html");
|
|
105
|
+
const preload = this.getAttribute("preload");
|
|
106
|
+
const partition = this.getAttribute("partition");
|
|
107
|
+
const renderer = (this.getAttribute("renderer") || "native") as
|
|
108
|
+
| "native"
|
|
109
|
+
| "cef";
|
|
110
|
+
const masks = this.getAttribute("masks");
|
|
111
|
+
const navigationRules = this.getInitialNavigationRules();
|
|
112
|
+
// Sandbox attribute: when present, the child webview is sandboxed (no RPC, events only)
|
|
113
|
+
const sandbox = this.hasAttribute("sandbox");
|
|
114
|
+
this.sandboxed = sandbox;
|
|
115
|
+
// Read transparent/passthrough attributes for initial state (avoids flash)
|
|
116
|
+
const transparent = this.hasAttribute("transparent");
|
|
117
|
+
const passthrough = this.hasAttribute("passthrough");
|
|
118
|
+
this.transparent = transparent;
|
|
119
|
+
this.passthroughEnabled = passthrough;
|
|
120
|
+
if (transparent) this.style.opacity = "0";
|
|
121
|
+
if (passthrough) this.style.pointerEvents = "none";
|
|
122
|
+
|
|
123
|
+
if (masks) {
|
|
124
|
+
masks.split(",").forEach((s) => this.maskSelectors.add(s.trim()));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const webviewInitParams = {
|
|
129
|
+
hostWebviewId: window.__sparkbunWebviewId,
|
|
130
|
+
windowId: window.__sparkbunWindowId,
|
|
131
|
+
renderer,
|
|
132
|
+
url,
|
|
133
|
+
html,
|
|
134
|
+
preload,
|
|
135
|
+
partition,
|
|
136
|
+
frame: {
|
|
137
|
+
width: rect.width,
|
|
138
|
+
height: rect.height,
|
|
139
|
+
x: rect.x,
|
|
140
|
+
y: rect.y,
|
|
141
|
+
},
|
|
142
|
+
sandbox,
|
|
143
|
+
transparent,
|
|
144
|
+
passthrough,
|
|
145
|
+
...(navigationRules === null ? {} : { navigationRules }),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const webviewId = (await request(
|
|
149
|
+
"webviewTagInit",
|
|
150
|
+
webviewInitParams,
|
|
151
|
+
)) as number;
|
|
152
|
+
|
|
153
|
+
this.webviewId = webviewId;
|
|
154
|
+
this.id = `sparkbun-webview-${webviewId}`;
|
|
155
|
+
webviewRegistry[webviewId] = this;
|
|
156
|
+
|
|
157
|
+
this.setupObservers(initialRect);
|
|
158
|
+
// Force immediate sync after initialization
|
|
159
|
+
this.syncDimensions(true);
|
|
160
|
+
|
|
161
|
+
// When adding a new webview, force all existing webviews to re-sync their positions
|
|
162
|
+
// This handles layout changes caused by the new webview
|
|
163
|
+
// Use requestAnimationFrame to ensure DOM layout is complete
|
|
164
|
+
requestAnimationFrame(() => {
|
|
165
|
+
Object.values(webviewRegistry).forEach((webview) => {
|
|
166
|
+
if (webview !== this && webview.webviewId !== null) {
|
|
167
|
+
webview.syncDimensions(true);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error("Failed to init webview:", err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setupObservers(initialRect: Rect) {
|
|
177
|
+
const getMasks = () => {
|
|
178
|
+
const rect = this.getBoundingClientRect();
|
|
179
|
+
const masks: Rect[] = [];
|
|
180
|
+
this.maskSelectors.forEach((selector) => {
|
|
181
|
+
try {
|
|
182
|
+
document.querySelectorAll(selector).forEach((el) => {
|
|
183
|
+
const mr = el.getBoundingClientRect();
|
|
184
|
+
masks.push({
|
|
185
|
+
x: mr.x - rect.x,
|
|
186
|
+
y: mr.y - rect.y,
|
|
187
|
+
width: mr.width,
|
|
188
|
+
height: mr.height,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
} catch (_e) {
|
|
192
|
+
// Invalid selector, ignore
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
return masks;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
this._sync = new OverlaySyncController(this, {
|
|
199
|
+
onSync: (rect, masksJson) => {
|
|
200
|
+
if (this.webviewId === null) return;
|
|
201
|
+
send("webviewTagResize", {
|
|
202
|
+
id: this.webviewId,
|
|
203
|
+
frame: rect,
|
|
204
|
+
masks: masksJson,
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
getMasks,
|
|
208
|
+
burstIntervalMs: 10,
|
|
209
|
+
baseIntervalMs: 100,
|
|
210
|
+
burstDurationMs: 50,
|
|
211
|
+
});
|
|
212
|
+
this._sync.setLastRect(initialRect);
|
|
213
|
+
this._sync.start();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
syncDimensions(force = false) {
|
|
217
|
+
if (!this._sync) return;
|
|
218
|
+
if (force) {
|
|
219
|
+
this._sync.forceSync();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Navigation methods
|
|
224
|
+
loadURL(url: string) {
|
|
225
|
+
if (this.webviewId === null) return;
|
|
226
|
+
this.setAttribute("src", url);
|
|
227
|
+
send("webviewTagUpdateSrc", { id: this.webviewId, url });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
loadHTML(html: string) {
|
|
231
|
+
if (this.webviewId === null) return;
|
|
232
|
+
send("webviewTagUpdateHtml", { id: this.webviewId, html });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
reload() {
|
|
236
|
+
if (this.webviewId !== null)
|
|
237
|
+
send("webviewTagReload", { id: this.webviewId });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
goBack() {
|
|
241
|
+
if (this.webviewId !== null)
|
|
242
|
+
send("webviewTagGoBack", { id: this.webviewId });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
goForward() {
|
|
246
|
+
if (this.webviewId !== null)
|
|
247
|
+
send("webviewTagGoForward", { id: this.webviewId });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async canGoBack(): Promise<boolean> {
|
|
251
|
+
if (this.webviewId === null) return false;
|
|
252
|
+
return (await request("webviewTagCanGoBack", {
|
|
253
|
+
id: this.webviewId,
|
|
254
|
+
})) as boolean;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async canGoForward(): Promise<boolean> {
|
|
258
|
+
if (this.webviewId === null) return false;
|
|
259
|
+
return (await request("webviewTagCanGoForward", {
|
|
260
|
+
id: this.webviewId,
|
|
261
|
+
})) as boolean;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Visibility methods
|
|
265
|
+
toggleTransparent(value?: boolean) {
|
|
266
|
+
if (this.webviewId === null) return;
|
|
267
|
+
this.transparent = value !== undefined ? value : !this.transparent;
|
|
268
|
+
this.style.opacity = this.transparent ? "0" : "";
|
|
269
|
+
send("webviewTagSetTransparent", {
|
|
270
|
+
id: this.webviewId,
|
|
271
|
+
transparent: this.transparent,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
togglePassthrough(value?: boolean) {
|
|
276
|
+
if (this.webviewId === null) return;
|
|
277
|
+
this.passthroughEnabled =
|
|
278
|
+
value !== undefined ? value : !this.passthroughEnabled;
|
|
279
|
+
this.style.pointerEvents = this.passthroughEnabled ? "none" : "";
|
|
280
|
+
send("webviewTagSetPassthrough", {
|
|
281
|
+
id: this.webviewId,
|
|
282
|
+
enablePassthrough: this.passthroughEnabled,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
toggleHidden(value?: boolean) {
|
|
287
|
+
if (this.webviewId === null) return;
|
|
288
|
+
this.hidden = value !== undefined ? value : !this.hidden;
|
|
289
|
+
send("webviewTagSetHidden", { id: this.webviewId, hidden: this.hidden });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Mask management
|
|
293
|
+
addMaskSelector(selector: string) {
|
|
294
|
+
this.maskSelectors.add(selector);
|
|
295
|
+
this.syncDimensions(true);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
removeMaskSelector(selector: string) {
|
|
299
|
+
this.maskSelectors.delete(selector);
|
|
300
|
+
this.syncDimensions(true);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Navigation rules
|
|
304
|
+
setNavigationRules(rules: string[]) {
|
|
305
|
+
if (this.webviewId !== null) {
|
|
306
|
+
send("webviewTagSetNavigationRules", { id: this.webviewId, rules });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Find in page
|
|
311
|
+
findInPage(
|
|
312
|
+
searchText: string,
|
|
313
|
+
options?: { forward?: boolean; matchCase?: boolean },
|
|
314
|
+
) {
|
|
315
|
+
if (this.webviewId === null) return;
|
|
316
|
+
const forward = options?.forward !== false;
|
|
317
|
+
const matchCase = options?.matchCase || false;
|
|
318
|
+
send("webviewTagFindInPage", {
|
|
319
|
+
id: this.webviewId,
|
|
320
|
+
searchText,
|
|
321
|
+
forward,
|
|
322
|
+
matchCase,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
stopFindInPage() {
|
|
327
|
+
if (this.webviewId !== null)
|
|
328
|
+
send("webviewTagStopFind", { id: this.webviewId });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// DevTools
|
|
332
|
+
openDevTools() {
|
|
333
|
+
if (this.webviewId !== null)
|
|
334
|
+
send("webviewTagOpenDevTools", { id: this.webviewId });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
closeDevTools() {
|
|
338
|
+
if (this.webviewId !== null)
|
|
339
|
+
send("webviewTagCloseDevTools", { id: this.webviewId });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
toggleDevTools() {
|
|
343
|
+
if (this.webviewId !== null)
|
|
344
|
+
send("webviewTagToggleDevTools", { id: this.webviewId });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// JavaScript execution
|
|
348
|
+
executeJavascript(js: string) {
|
|
349
|
+
if (this.webviewId === null) return;
|
|
350
|
+
send("webviewTagExecuteJavascript", { id: this.webviewId, js });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Event handling
|
|
354
|
+
on(event: WebviewEventType, listener: (event: CustomEvent) => void) {
|
|
355
|
+
if (!this._eventListeners[event]) this._eventListeners[event] = [];
|
|
356
|
+
this._eventListeners[event].push(listener);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
off(event: WebviewEventType, listener: (event: CustomEvent) => void) {
|
|
360
|
+
if (!this._eventListeners[event]) return;
|
|
361
|
+
const idx = this._eventListeners[event].indexOf(listener);
|
|
362
|
+
if (idx !== -1) this._eventListeners[event].splice(idx, 1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
emit(event: WebviewEventType, detail: unknown) {
|
|
366
|
+
const listeners = this._eventListeners[event];
|
|
367
|
+
if (listeners) {
|
|
368
|
+
const customEvent = new CustomEvent(event, { detail });
|
|
369
|
+
listeners.forEach((fn) => fn(customEvent));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Property getters/setters
|
|
374
|
+
get src(): string | null {
|
|
375
|
+
return this.getAttribute("src");
|
|
376
|
+
}
|
|
377
|
+
set src(value: string | null) {
|
|
378
|
+
if (value) {
|
|
379
|
+
this.setAttribute("src", value);
|
|
380
|
+
} else {
|
|
381
|
+
this.removeAttribute("src");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
get html(): string | null {
|
|
386
|
+
return this.getAttribute("html");
|
|
387
|
+
}
|
|
388
|
+
set html(value: string | null) {
|
|
389
|
+
if (value) {
|
|
390
|
+
this.setAttribute("html", value);
|
|
391
|
+
} else {
|
|
392
|
+
this.removeAttribute("html");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
get preload(): string | null {
|
|
397
|
+
return this.getAttribute("preload");
|
|
398
|
+
}
|
|
399
|
+
set preload(value: string | null) {
|
|
400
|
+
if (value) this.setAttribute("preload", value);
|
|
401
|
+
else this.removeAttribute("preload");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
get renderer(): "native" | "cef" {
|
|
405
|
+
return (this.getAttribute("renderer") as "native" | "cef") || "native";
|
|
406
|
+
}
|
|
407
|
+
set renderer(value: "native" | "cef") {
|
|
408
|
+
this.setAttribute("renderer", value);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Sandbox is read-only after creation (set via attribute before adding to DOM)
|
|
412
|
+
get sandbox(): boolean {
|
|
413
|
+
return this.sandboxed;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function initWebviewTag() {
|
|
418
|
+
// Register the custom element if not already registered
|
|
419
|
+
if (!customElements.get("sparkbun-webview")) {
|
|
420
|
+
customElements.define("sparkbun-webview", SparkBunWebviewTag);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Add default styles for <sparkbun-webview> elements
|
|
424
|
+
// These can be easily overridden in the host document
|
|
425
|
+
const injectStyles = () => {
|
|
426
|
+
const style = document.createElement("style");
|
|
427
|
+
style.textContent = `
|
|
428
|
+
sparkbun-webview {
|
|
429
|
+
display: block;
|
|
430
|
+
width: 800px;
|
|
431
|
+
height: 300px;
|
|
432
|
+
background: #fff;
|
|
433
|
+
background-repeat: no-repeat !important;
|
|
434
|
+
overflow: hidden;
|
|
435
|
+
}
|
|
436
|
+
`;
|
|
437
|
+
// Insert at the beginning of <head> so app styles take precedence
|
|
438
|
+
if (document.head?.firstChild) {
|
|
439
|
+
document.head.insertBefore(style, document.head.firstChild);
|
|
440
|
+
} else if (document.head) {
|
|
441
|
+
document.head.appendChild(style);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// document.head may not exist at document start, defer if needed
|
|
446
|
+
if (document.head) {
|
|
447
|
+
injectStyles();
|
|
448
|
+
} else {
|
|
449
|
+
document.addEventListener("DOMContentLoaded", injectStyles);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// <sparkbun-wgpu> Custom Element
|
|
2
|
+
// Provides a layout-driven native WGPU view that is positioned via a DOM element.
|
|
3
|
+
|
|
4
|
+
import "./globals.d.ts";
|
|
5
|
+
import { send, request } from "./internalRpc";
|
|
6
|
+
import { OverlaySyncController, type Rect } from "./overlaySync";
|
|
7
|
+
|
|
8
|
+
type WgpuTagEventType = "ready";
|
|
9
|
+
|
|
10
|
+
// Registry for WGPU view instances (for event routing if needed)
|
|
11
|
+
export const wgpuTagRegistry: Record<number, SparkBunWgpuTag> = {};
|
|
12
|
+
|
|
13
|
+
export class SparkBunWgpuTag extends HTMLElement {
|
|
14
|
+
wgpuViewId: number | null = null;
|
|
15
|
+
maskSelectors: Set<string> = new Set();
|
|
16
|
+
private _sync: OverlaySyncController | null = null;
|
|
17
|
+
transparent = false;
|
|
18
|
+
passthroughEnabled = false;
|
|
19
|
+
hidden = false;
|
|
20
|
+
private _eventListeners: Record<string, Array<(event: CustomEvent) => void>> =
|
|
21
|
+
{};
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
super();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
connectedCallback() {
|
|
28
|
+
requestAnimationFrame(() => this.initWgpuView());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
disconnectedCallback() {
|
|
32
|
+
if (this.wgpuViewId !== null) {
|
|
33
|
+
send("wgpuTagRemove", { id: this.wgpuViewId });
|
|
34
|
+
delete wgpuTagRegistry[this.wgpuViewId];
|
|
35
|
+
}
|
|
36
|
+
if (this._sync) this._sync.stop();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async initWgpuView() {
|
|
40
|
+
const rect = this.getBoundingClientRect();
|
|
41
|
+
const initialRect = {
|
|
42
|
+
x: rect.x,
|
|
43
|
+
y: rect.y,
|
|
44
|
+
width: rect.width,
|
|
45
|
+
height: rect.height,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const transparent = this.hasAttribute("transparent");
|
|
49
|
+
const passthrough = this.hasAttribute("passthrough");
|
|
50
|
+
const hidden = this.hasAttribute("hidden");
|
|
51
|
+
const masks = this.getAttribute("masks");
|
|
52
|
+
|
|
53
|
+
this.transparent = transparent;
|
|
54
|
+
this.passthroughEnabled = passthrough;
|
|
55
|
+
this.hidden = hidden;
|
|
56
|
+
|
|
57
|
+
if (masks) {
|
|
58
|
+
masks.split(",").forEach((s) => this.maskSelectors.add(s.trim()));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (transparent) this.style.opacity = "0";
|
|
62
|
+
if (passthrough) this.style.pointerEvents = "none";
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const wgpuViewId = (await request("wgpuTagInit", {
|
|
66
|
+
windowId: window.__sparkbunWindowId,
|
|
67
|
+
frame: {
|
|
68
|
+
width: rect.width,
|
|
69
|
+
height: rect.height,
|
|
70
|
+
x: rect.x,
|
|
71
|
+
y: rect.y,
|
|
72
|
+
},
|
|
73
|
+
transparent,
|
|
74
|
+
passthrough,
|
|
75
|
+
})) as number;
|
|
76
|
+
|
|
77
|
+
this.wgpuViewId = wgpuViewId;
|
|
78
|
+
this.id = `sparkbun-wgpu-${wgpuViewId}`;
|
|
79
|
+
wgpuTagRegistry[wgpuViewId] = this;
|
|
80
|
+
|
|
81
|
+
this.setupObservers(initialRect);
|
|
82
|
+
// Force immediate sync after initialization
|
|
83
|
+
this.syncDimensions(true);
|
|
84
|
+
|
|
85
|
+
// Apply hidden state after creation (no init flag for hidden)
|
|
86
|
+
if (hidden) {
|
|
87
|
+
this.toggleHidden(true);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// When adding a new WGPU view, force all existing WGPU views to re-sync
|
|
91
|
+
requestAnimationFrame(() => {
|
|
92
|
+
Object.values(wgpuTagRegistry).forEach((view) => {
|
|
93
|
+
if (view !== this && view.wgpuViewId !== null) {
|
|
94
|
+
view.syncDimensions(true);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.emit("ready", { id: wgpuViewId });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error("Failed to init WGPU view:", err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setupObservers(initialRect: Rect) {
|
|
106
|
+
const getMasks = () => {
|
|
107
|
+
const rect = this.getBoundingClientRect();
|
|
108
|
+
const masks: Rect[] = [];
|
|
109
|
+
this.maskSelectors.forEach((selector) => {
|
|
110
|
+
try {
|
|
111
|
+
document.querySelectorAll(selector).forEach((el) => {
|
|
112
|
+
const mr = el.getBoundingClientRect();
|
|
113
|
+
masks.push({
|
|
114
|
+
x: mr.x - rect.x,
|
|
115
|
+
y: mr.y - rect.y,
|
|
116
|
+
width: mr.width,
|
|
117
|
+
height: mr.height,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
} catch (_e) {
|
|
121
|
+
// Invalid selector, ignore
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return masks;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
this._sync = new OverlaySyncController(this, {
|
|
128
|
+
onSync: (rect, masksJson) => {
|
|
129
|
+
if (this.wgpuViewId === null) return;
|
|
130
|
+
send("wgpuTagResize", {
|
|
131
|
+
id: this.wgpuViewId,
|
|
132
|
+
frame: rect,
|
|
133
|
+
masks: masksJson,
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
getMasks,
|
|
137
|
+
burstIntervalMs: 10,
|
|
138
|
+
baseIntervalMs: 100,
|
|
139
|
+
burstDurationMs: 50,
|
|
140
|
+
});
|
|
141
|
+
this._sync.setLastRect(initialRect);
|
|
142
|
+
this._sync.start();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
syncDimensions(force = false) {
|
|
146
|
+
if (!this._sync) return;
|
|
147
|
+
if (force) {
|
|
148
|
+
this._sync.forceSync();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Visibility methods
|
|
153
|
+
toggleTransparent(value?: boolean) {
|
|
154
|
+
if (this.wgpuViewId === null) return;
|
|
155
|
+
this.transparent = value !== undefined ? value : !this.transparent;
|
|
156
|
+
this.style.opacity = this.transparent ? "0" : "";
|
|
157
|
+
send("wgpuTagSetTransparent", {
|
|
158
|
+
id: this.wgpuViewId,
|
|
159
|
+
transparent: this.transparent,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
togglePassthrough(value?: boolean) {
|
|
164
|
+
if (this.wgpuViewId === null) return;
|
|
165
|
+
this.passthroughEnabled =
|
|
166
|
+
value !== undefined ? value : !this.passthroughEnabled;
|
|
167
|
+
this.style.pointerEvents = this.passthroughEnabled ? "none" : "";
|
|
168
|
+
send("wgpuTagSetPassthrough", {
|
|
169
|
+
id: this.wgpuViewId,
|
|
170
|
+
passthrough: this.passthroughEnabled,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
toggleHidden(value?: boolean) {
|
|
175
|
+
if (this.wgpuViewId === null) return;
|
|
176
|
+
this.hidden = value !== undefined ? value : !this.hidden;
|
|
177
|
+
send("wgpuTagSetHidden", { id: this.wgpuViewId, hidden: this.hidden });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Debug helper (native test renderer)
|
|
181
|
+
runTest() {
|
|
182
|
+
if (this.wgpuViewId === null) return;
|
|
183
|
+
send("wgpuTagRunTest", { id: this.wgpuViewId });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Mask management
|
|
187
|
+
addMaskSelector(selector: string) {
|
|
188
|
+
this.maskSelectors.add(selector);
|
|
189
|
+
this.syncDimensions(true);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
removeMaskSelector(selector: string) {
|
|
193
|
+
this.maskSelectors.delete(selector);
|
|
194
|
+
this.syncDimensions(true);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Event handling
|
|
198
|
+
on(event: WgpuTagEventType, listener: (event: CustomEvent) => void) {
|
|
199
|
+
if (!this._eventListeners[event]) this._eventListeners[event] = [];
|
|
200
|
+
this._eventListeners[event].push(listener);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
off(event: WgpuTagEventType, listener: (event: CustomEvent) => void) {
|
|
204
|
+
if (!this._eventListeners[event]) return;
|
|
205
|
+
const idx = this._eventListeners[event].indexOf(listener);
|
|
206
|
+
if (idx !== -1) this._eventListeners[event].splice(idx, 1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
emit(event: WgpuTagEventType, detail: unknown) {
|
|
210
|
+
const listeners = this._eventListeners[event];
|
|
211
|
+
if (listeners) {
|
|
212
|
+
const customEvent = new CustomEvent(event, { detail });
|
|
213
|
+
listeners.forEach((fn) => fn(customEvent));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function initWgpuTag() {
|
|
219
|
+
if (!customElements.get("sparkbun-wgpu")) {
|
|
220
|
+
customElements.define("sparkbun-wgpu", SparkBunWgpuTag);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const injectStyles = () => {
|
|
224
|
+
const style = document.createElement("style");
|
|
225
|
+
style.textContent = `
|
|
226
|
+
sparkbun-wgpu {
|
|
227
|
+
display: block;
|
|
228
|
+
width: 800px;
|
|
229
|
+
height: 300px;
|
|
230
|
+
background: #000;
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
}
|
|
233
|
+
`;
|
|
234
|
+
if (document.head?.firstChild) {
|
|
235
|
+
document.head.insertBefore(style, document.head.firstChild);
|
|
236
|
+
} else if (document.head) {
|
|
237
|
+
document.head.appendChild(style);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (document.head) {
|
|
242
|
+
injectStyles();
|
|
243
|
+
} else {
|
|
244
|
+
document.addEventListener("DOMContentLoaded", injectStyles);
|
|
245
|
+
}
|
|
246
|
+
}
|