electrobun 0.0.18 → 0.0.19-beta.6

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.
@@ -1,650 +0,0 @@
1
- type WebviewEventTypes =
2
- | "did-navigate"
3
- | "did-navigate-in-page"
4
- | "did-commit-navigation"
5
- | "dom-ready";
6
-
7
- type Rect = { x: number; y: number; width: number; height: number };
8
-
9
- const ConfigureWebviewTags = (
10
- enableWebviewTags: boolean,
11
- zigRpc: (params: any) => any,
12
- syncRpc: (params: any) => any
13
- ) => {
14
- if (!enableWebviewTags) {
15
- return;
16
- }
17
-
18
- // todo: provide global types for <electrobun-webview> tag elements (like querySelector results etc.)
19
-
20
- class WebviewTag extends HTMLElement {
21
- // todo (yoav): come up with a better mechanism to eliminate collisions with bun created
22
- // webviews
23
- webviewId?: number; // = nextWebviewId++;
24
-
25
- // rpc
26
- zigRpc: any;
27
- syncRpc: any;
28
-
29
- // querySelectors for elements that you want to appear
30
- // in front of the webview.
31
- maskSelectors: Set<string> = new Set();
32
-
33
- // observers
34
- resizeObserver?: ResizeObserver;
35
- // intersectionObserver?: IntersectionObserver;
36
- // mutationObserver?: MutationObserver;
37
-
38
- positionCheckLoop?: Timer;
39
- positionCheckLoopReset?: Timer;
40
-
41
- lastRect = {
42
- x: 0,
43
- y: 0,
44
- width: 0,
45
- height: 0,
46
- };
47
-
48
- lastMasksJSON: string = "";
49
- lastMasks: Rect[] = [];
50
-
51
- transparent: boolean = false;
52
- passthroughEnabled: boolean = false;
53
- hidden: boolean = false;
54
- delegateMode: boolean = false;
55
- hiddenMirrorMode: boolean = false;
56
- wasZeroRect: boolean = false;
57
- isMirroring: boolean = false;
58
-
59
- partition: string | null = null;
60
-
61
- constructor() {
62
- super();
63
- this.zigRpc = zigRpc;
64
- this.syncRpc = syncRpc;
65
-
66
- // Give it a frame to be added to the dom and render before measuring
67
- requestAnimationFrame(() => {
68
- this.initWebview();
69
- });
70
- }
71
-
72
- addMaskSelector(selector: string) {
73
- this.maskSelectors.add(selector);
74
- this.syncDimensions();
75
- }
76
-
77
- removeMaskSelector(selector: string) {
78
- this.maskSelectors.delete(selector);
79
- this.syncDimensions();
80
- }
81
-
82
- initWebview() {
83
- const rect = this.getBoundingClientRect();
84
- this.lastRect = rect;
85
-
86
- const webviewId = this.syncRpc({
87
- method: "webviewTagInit",
88
- params: {
89
- hostWebviewId: window.__electrobunWebviewId,
90
- windowId: window.__electrobunWindowId,
91
- url: this.src || this.getAttribute("src") || null,
92
- html: this.html || this.getAttribute("html") || null,
93
- preload: this.preload || this.getAttribute("preload") || null,
94
- partition: this.partition || this.getAttribute("partition") || null,
95
- frame: {
96
- width: rect.width,
97
- height: rect.height,
98
- x: rect.x,
99
- y: rect.y,
100
- },
101
- },
102
- });
103
-
104
- this.webviewId = webviewId;
105
- this.id = `electrobun-webview-${webviewId}`;
106
- // todo: replace zig -> webviewtag communication with a global instead of
107
- // queryselector based on id
108
- this.setAttribute("id", this.id);
109
- }
110
-
111
- asyncResolvers: {
112
- [id: string]: { resolve: (arg: any) => void; reject: (arg: any) => void };
113
- } = {};
114
-
115
- callAsyncJavaScript({ script }: { script: string }) {
116
- return new Promise((resolve, reject) => {
117
- const messageId = "" + Date.now() + Math.random();
118
- this.asyncResolvers[messageId] = {
119
- resolve,
120
- reject,
121
- };
122
-
123
- this.zigRpc.request.webviewTagCallAsyncJavaScript({
124
- messageId,
125
- webviewId: this.webviewId,
126
- hostWebviewId: window.__electrobunWebviewId,
127
- script,
128
- });
129
- });
130
- }
131
-
132
- setCallAsyncJavaScriptResponse(messageId: string, response: any) {
133
- const resolvers = this.asyncResolvers[messageId];
134
- delete this.asyncResolvers[messageId];
135
- try {
136
- response = JSON.parse(response);
137
-
138
- if (response.result) {
139
- resolvers.resolve(response.result);
140
- } else {
141
- resolvers.reject(response.error);
142
- }
143
- } catch (e: any) {
144
- resolvers.reject(e.message);
145
- }
146
- }
147
-
148
- async canGoBack() {
149
- const {
150
- payload: { webviewTagCanGoBackResponse },
151
- } = await this.zigRpc.request.webviewTagCanGoBack({ id: this.webviewId });
152
- return webviewTagCanGoBackResponse;
153
- }
154
-
155
- async canGoForward() {
156
- const {
157
- payload: { webviewTagCanGoForwardResponse },
158
- } = await this.zigRpc.request.webviewTagCanGoForward({
159
- id: this.webviewId,
160
- });
161
- return webviewTagCanGoForwardResponse;
162
- }
163
-
164
- // propertie setters/getters. keeps them in sync with dom attributes
165
- updateAttr(name: string, value: string | null) {
166
- if (value) {
167
- this.setAttribute(name, value);
168
- } else {
169
- this.removeAttribute(name);
170
- }
171
- }
172
-
173
- get src() {
174
- return this.getAttribute("src");
175
- }
176
-
177
- set src(value) {
178
- this.updateAttr("src", value);
179
- }
180
-
181
- get html() {
182
- return this.getAttribute("html");
183
- }
184
-
185
- set html(value) {
186
- this.updateAttr("html", value);
187
- }
188
-
189
- get preload() {
190
- return this.getAttribute("preload");
191
- }
192
-
193
- set preload(value) {
194
- this.updateAttr("preload", value);
195
- }
196
-
197
- // Note: since <electrobun-webview> is an anchor for a native webview
198
- // on osx even if we hide it, enable mouse passthrough etc. There
199
- // are still events like drag events which are natively handled deep in the window manager
200
- // and will be handled incorrectly. To get around this for now we need to
201
- // move the webview off screen during delegate mode.
202
- adjustDimensionsForHiddenMirrorMode(rect: DOMRect) {
203
- if (this.hiddenMirrorMode) {
204
- rect.x = 0 - rect.width;
205
- }
206
-
207
- return rect;
208
- }
209
-
210
- // Note: in the brwoser-context we can ride on the dom element's uilt in event emitter for managing custom events
211
- on(event: WebviewEventTypes, listener: () => {}) {
212
- this.addEventListener(event, listener);
213
- }
214
-
215
- off(event: WebviewEventTypes, listener: () => {}) {
216
- this.removeEventListener(event, listener);
217
- }
218
-
219
- // This is typically called by injected js from zig
220
- emit(event: WebviewEventTypes, detail: any) {
221
- this.dispatchEvent(new CustomEvent(event, { detail }));
222
- }
223
-
224
- // Call this via document.querySelector('electrobun-webview').syncDimensions();
225
- // That way the host can trigger an alignment with the nested webview when they
226
- // know that they're chaning something in order to eliminate the lag that the
227
- // catch all loop will catch
228
- syncDimensions(force: boolean = false) {
229
- if (!this.webviewId || (!force && this.hidden)) {
230
- return;
231
- }
232
-
233
- const rect = this.getBoundingClientRect();
234
- const { x, y, width, height } =
235
- this.adjustDimensionsForHiddenMirrorMode(rect);
236
- const lastRect = this.lastRect;
237
-
238
- if (width === 0 && height === 0) {
239
- if (this.wasZeroRect === false) {
240
- this.wasZeroRect = true;
241
- this.toggleHidden(true, true);
242
- }
243
- return;
244
- }
245
-
246
- const masks: Rect[] = [];
247
- this.maskSelectors.forEach((selector) => {
248
- const els = document.querySelectorAll(selector);
249
-
250
- for (let i = 0; i < els.length; i++) {
251
- const el = els[i];
252
-
253
- if (el) {
254
- const maskRect = el.getBoundingClientRect();
255
-
256
- masks.push({
257
- // reposition the bounding rect to be relative to the webview rect
258
- // so objc can apply the mask correctly and handle the actual overlap
259
- x: maskRect.x - x,
260
- y: maskRect.y - y,
261
- width: maskRect.width,
262
- height: maskRect.height,
263
- });
264
- }
265
- }
266
- });
267
-
268
- // store jsonStringified last masks value to compare
269
- const masksJson = masks.length ? JSON.stringify(masks) : "";
270
-
271
- if (
272
- force ||
273
- lastRect.x !== x ||
274
- lastRect.y !== y ||
275
- lastRect.width !== width ||
276
- lastRect.height !== height ||
277
- this.lastMasksJSON !== masksJson
278
- ) {
279
- // let it know we're still accelerating
280
- this.setPositionCheckLoop(true);
281
-
282
- this.lastRect = rect;
283
- this.lastMasks = masks;
284
- this.lastMasksJSON = masksJson;
285
-
286
- this.zigRpc.send.webviewTagResize({
287
- id: this.webviewId,
288
- frame: {
289
- width: width,
290
- height: height,
291
- x: x,
292
- y: y,
293
- },
294
- masks: masksJson,
295
- });
296
- }
297
-
298
- if (this.wasZeroRect) {
299
- this.wasZeroRect = false;
300
- this.toggleHidden(false, true);
301
- }
302
- }
303
-
304
- boundSyncDimensions = () => this.syncDimensions();
305
- boundForceSyncDimensions = () => this.syncDimensions(true);
306
-
307
- setPositionCheckLoop(accelerate = false) {
308
- if (this.positionCheckLoop) {
309
- clearInterval(this.positionCheckLoop);
310
- this.positionCheckLoop = undefined;
311
- }
312
-
313
- if (this.positionCheckLoopReset) {
314
- clearTimeout(this.positionCheckLoopReset);
315
- this.positionCheckLoopReset = undefined;
316
- }
317
-
318
- const delay = accelerate ? 0 : 300;
319
-
320
- if (accelerate) {
321
- this.positionCheckLoopReset = setTimeout(() => {
322
- this.setPositionCheckLoop(false);
323
- }, 2000);
324
- }
325
- // Note: Since there's not catch all way to listen for x/y changes
326
- // we have a 400ms interval to check
327
- // on m1 max this 400ms interval for one nested webview
328
- // only uses around 0.1% cpu
329
-
330
- // Note: We also listen for resize events and changes to
331
- // certain properties to get reactive repositioning for
332
- // many cases.
333
-
334
- // todo: consider having an option to disable this and let user
335
- // trigger position sync for high performance cases (like
336
- // a browser with a hundred tabs)
337
- this.positionCheckLoop = setInterval(() => this.syncDimensions(), delay);
338
- }
339
-
340
- connectedCallback() {
341
- this.setPositionCheckLoop();
342
-
343
- this.resizeObserver = new ResizeObserver(() => {
344
- this.syncDimensions();
345
- });
346
- // Note: In objc the webview is positioned in the window from the bottom-left corner
347
- // the html anchor is positioned in the webview from the top-left corner
348
- // In those cases the getBoundingClientRect() will return the same value, but
349
- // we still need to send it to objc to calculate from its bottom left position
350
- // otherwise it'll move around unexpectedly.
351
- window.addEventListener("resize", this.boundForceSyncDimensions);
352
- window.addEventListener("scroll", this.boundSyncDimensions);
353
-
354
- // todo: For chromium webviews (windows native or chromium bundled)
355
- // should be able to use performanceObservers on layout-shift to
356
- // call syncDimensions more reactively
357
- }
358
-
359
- disconnectedCallback() {
360
- // removed from the dom
361
- clearInterval(this.positionCheckLoop);
362
-
363
- this.resizeObserver?.disconnect();
364
- // this.intersectionObserver?.disconnect();
365
- // this.mutationObserver?.disconnect();
366
- window.removeEventListener("resize", this.boundForceSyncDimensions);
367
- window.removeEventListener("scroll", this.boundSyncDimensions);
368
- this.zigRpc.send.webviewTagRemove({ id: this.webviewId });
369
- }
370
-
371
- static get observedAttributes() {
372
- // TODO: support html, preload, and other stuff here
373
- return ["src", "html", "preload", "class", "style"];
374
- }
375
-
376
- attributeChangedCallback(name, oldValue, newValue) {
377
- if (name === "src" && oldValue !== newValue) {
378
- this.updateIFrameSrc(newValue);
379
- } else if (name === "html" && oldValue !== newValue) {
380
- this.updateIFrameHtml(newValue);
381
- } else if (name === "preload" && oldValue !== newValue) {
382
- this.updateIFramePreload(newValue);
383
- } else {
384
- this.syncDimensions();
385
- }
386
- }
387
-
388
- updateIFrameSrc(src: string) {
389
- if (!this.webviewId) {
390
- return;
391
- }
392
- this.zigRpc.send.webviewTagUpdateSrc({
393
- id: this.webviewId,
394
- url: src,
395
- });
396
- }
397
-
398
- updateIFrameHtml(html: string) {
399
- if (!this.webviewId) {
400
- return;
401
- }
402
- this.zigRpc.send.webviewTagUpdateHtml({
403
- id: this.webviewId,
404
- html,
405
- });
406
- }
407
-
408
- updateIFramePreload(preload: string) {
409
- if (!this.webviewId) {
410
- return;
411
- }
412
- this.zigRpc.send.webviewTagUpdatePreload({
413
- id: this.webviewId,
414
- preload,
415
- });
416
- }
417
-
418
- goBack() {
419
- this.zigRpc.send.webviewTagGoBack({ id: this.webviewId });
420
- }
421
-
422
- goForward() {
423
- this.zigRpc.send.webviewTagGoForward({ id: this.webviewId });
424
- }
425
-
426
- reload() {
427
- this.zigRpc.send.webviewTagReload({ id: this.webviewId });
428
- }
429
- loadURL(url: string) {
430
- this.setAttribute("src", url);
431
- this.zigRpc.send.webviewTagUpdateSrc({
432
- id: this.webviewId,
433
- url,
434
- });
435
- }
436
- // Note: you can set an interval and do this 60 times a second and it's pretty smooth
437
- // but it uses quite a bit of cpu
438
- // todo: change this to "mirror to dom" or something
439
- syncScreenshot(callback?: () => void) {
440
- const cacheBustString = `?${Date.now()}`;
441
- const url = `views://screenshot/${this.webviewId}${cacheBustString}`;
442
- const img = new Image();
443
- img.src = url;
444
- img.onload = () => {
445
- this.style.backgroundImage = `url(${url})`;
446
- if (callback) {
447
- // We've preloaded the image, but we still want to give it a chance to render
448
- // after setting the background style. give it quite a bit longer than a rafr
449
- setTimeout(callback, 100);
450
- }
451
- };
452
- }
453
-
454
- DEFAULT_FRAME_RATE = Math.round(1000 / 30); // 30fps
455
- streamScreenInterval?: Timer;
456
-
457
- // NOTE: This is very cpu intensive, Prefer startMirroring where possible
458
- startMirroringToDom(frameRate: number = this.DEFAULT_FRAME_RATE) {
459
- if (this.streamScreenInterval) {
460
- clearInterval(this.streamScreenInterval);
461
- }
462
-
463
- this.streamScreenInterval = setInterval(() => {
464
- this.syncScreenshot();
465
- }, frameRate);
466
- }
467
-
468
- stopMirroringToDom() {
469
- if (this.streamScreenInterval) {
470
- clearInterval(this.streamScreenInterval);
471
- this.streamScreenInterval = undefined;
472
- }
473
- }
474
-
475
- startMirroring() {
476
- // TEMP: mirroring now happens automatically in objc
477
- // when the mouse moves. I'm leaving this here for now
478
- // because I suspect there may still be use cases to
479
- // toggle it from the dom outside of the mouse moving.
480
- return;
481
- if (this.isMirroring === false) {
482
- this.isMirroring = true;
483
- this.zigRpc.send.webviewTagToggleMirroring({
484
- id: this.webviewId,
485
- enable: true,
486
- });
487
- }
488
- }
489
-
490
- stopMirroring() {
491
- return;
492
- if (this.isMirroring === true) {
493
- this.isMirroring = false;
494
- this.zigRpc.send.webviewTagToggleMirroring({
495
- id: this.webviewId,
496
- enable: false,
497
- });
498
- }
499
- }
500
-
501
- clearScreenImage() {
502
- this.style.backgroundImage = "";
503
- }
504
-
505
- tryClearScreenImage() {
506
- if (
507
- !this.transparent &&
508
- !this.hiddenMirrorMode &&
509
- !this.delegateMode &&
510
- !this.hidden
511
- ) {
512
- this.clearScreenImage();
513
- }
514
- }
515
- // This sets the native webview hovering over the dom to be transparent
516
- toggleTransparent(transparent?: boolean, bypassState?: boolean) {
517
- if (!bypassState) {
518
- if (typeof transparent === "undefined") {
519
- this.transparent = !this.transparent;
520
- } else {
521
- this.transparent = transparent;
522
- }
523
- }
524
-
525
- if (!this.transparent && !transparent) {
526
- this.tryClearScreenImage();
527
- }
528
-
529
- this.zigRpc.send.webviewTagSetTransparent({
530
- id: this.webviewId,
531
- transparent: this.transparent || Boolean(transparent),
532
- });
533
- }
534
- togglePassthrough(enablePassthrough?: boolean, bypassState?: boolean) {
535
- if (!bypassState) {
536
- if (typeof enablePassthrough === "undefined") {
537
- this.passthroughEnabled = !this.passthroughEnabled;
538
- } else {
539
- this.passthroughEnabled = enablePassthrough;
540
- }
541
- }
542
-
543
- this.zigRpc.send.webviewTagSetPassthrough({
544
- id: this.webviewId,
545
- enablePassthrough:
546
- this.passthroughEnabled || Boolean(enablePassthrough),
547
- });
548
- }
549
-
550
- toggleHidden(hidden?: boolean, bypassState?: boolean) {
551
- if (!bypassState) {
552
- if (typeof hidden === "undefined") {
553
- this.hidden = !this.hidden;
554
- } else {
555
- this.hidden = hidden;
556
- }
557
- }
558
-
559
- this.zigRpc.send.webviewTagSetHidden({
560
- id: this.webviewId,
561
- hidden: this.hidden || Boolean(hidden),
562
- });
563
- }
564
-
565
- // note: delegateMode and hiddenMirrorMode are experimental
566
- // ideally delegate mode would move the webview off screen
567
- // and delegate mouse and keyboard events to the webview while
568
- // streaming the screen so it can be fully layered in the dom
569
- // and fully interactive.
570
- toggleDelegateMode(delegateMode?: boolean) {
571
- const _newDelegateMode =
572
- typeof delegateMode === "undefined" ? !this.delegateMode : delegateMode;
573
-
574
- if (_newDelegateMode) {
575
- this.syncScreenshot(() => {
576
- this.delegateMode = true;
577
- this.toggleTransparent(true, true);
578
- this.startMirroringToDom();
579
- });
580
- } else {
581
- this.delegateMode = false;
582
- this.stopMirroringToDom();
583
- this.toggleTransparent(this.transparent);
584
- this.tryClearScreenImage();
585
- }
586
- }
587
-
588
- // While hiddenMirroMode would be similar to delegate mode but non-interactive
589
- // This is used while scrolling or resizing the <electrobun-webviewtag> to
590
- // make it smoother (scrolls with the dom) but disables interaction so that
591
- // during the scroll we don't need to worry about the webview being misaligned
592
- // with the mirror and accidentlly clicking on the wrong thing.
593
- toggleHiddenMirrorMode(force: boolean) {
594
- const enable =
595
- typeof force === "undefined" ? !this.hiddenMirrorMode : force;
596
-
597
- if (enable === true) {
598
- this.syncScreenshot(() => {
599
- this.hiddenMirrorMode = true;
600
- this.toggleHidden(true, true);
601
- this.togglePassthrough(true, true);
602
- this.startMirroringToDom();
603
- });
604
- } else {
605
- this.stopMirroringToDom();
606
- this.toggleHidden(this.hidden);
607
- this.togglePassthrough(this.passthroughEnabled);
608
- this.tryClearScreenImage();
609
- this.hiddenMirrorMode = false;
610
- }
611
- }
612
- }
613
-
614
- customElements.define("electrobun-webview", WebviewTag);
615
-
616
- insertWebviewTagNormalizationStyles();
617
- };
618
-
619
- // Give <electrobun-webview>s some default styles that can
620
- // be easily overridden in the host document
621
- const insertWebviewTagNormalizationStyles = () => {
622
- var style = document.createElement("style");
623
- style.type = "text/css";
624
-
625
- var css = `
626
- electrobun-webview {
627
- display: block;
628
- width: 800px;
629
- height: 300px;
630
- background: #fff;
631
- background-repeat: no-repeat!important;
632
- overflow: hidden;
633
- }
634
- `;
635
-
636
- style.appendChild(document.createTextNode(css));
637
-
638
- var head = document.getElementsByTagName("head")[0];
639
- if (!head) {
640
- return;
641
- }
642
-
643
- if (head.firstChild) {
644
- head.insertBefore(style, head.firstChild);
645
- } else {
646
- head.appendChild(style);
647
- }
648
- };
649
-
650
- export { ConfigureWebviewTags };
@@ -1,66 +0,0 @@
1
- import { zigRPC, type ApplicationMenuItemConfig } from "../proc/zig";
2
- import electrobunEventEmitter from "../events/eventEmitter";
3
-
4
- export const setApplicationMenu = (menu: Array<ApplicationMenuItemConfig>) => {
5
- const menuWithDefaults = menuConfigWithDefaults(menu);
6
- zigRPC.request.setApplicationMenu({
7
- menuConfig: JSON.stringify(menuWithDefaults),
8
- });
9
- };
10
-
11
- export const on = (name: "application-menu-clicked", handler) => {
12
- const specificName = `${name}`;
13
- electrobunEventEmitter.on(specificName, handler);
14
- };
15
-
16
- const roleLabelMap = {
17
- quit: "Quit",
18
- hide: "Hide",
19
- hideOthers: "Hide Others",
20
- showAll: "Show All",
21
- undo: "Undo",
22
- redo: "Redo",
23
- cut: "Cut",
24
- copy: "Copy",
25
- paste: "Paste",
26
- pasteAndMatchStyle: "Paste And Match Style",
27
- delete: "Delete",
28
- selectAll: "Select All",
29
- startSpeaking: "Start Speaking",
30
- stopSpeaking: "Stop Speaking",
31
- enterFullScreen: "Enter FullScreen",
32
- exitFullScreen: "Exit FullScreen",
33
- toggleFullScreen: "Toggle Full Screen",
34
- minimize: "Minimize",
35
- zoom: "Zoom",
36
- bringAllToFront: "Bring All To Front",
37
- close: "Close",
38
- cycleThroughWindows: "Cycle Through Windows",
39
- showHelp: "Show Help",
40
- };
41
-
42
- const menuConfigWithDefaults = (
43
- menu: Array<ApplicationMenuItemConfig>
44
- ): Array<ApplicationMenuItemConfig> => {
45
- return menu.map((item) => {
46
- if (item.type === "divider" || item.type === "separator") {
47
- return { type: "divider" };
48
- } else {
49
- return {
50
- label: item.label || roleLabelMap[item.role] || "",
51
- type: item.type || "normal",
52
- // application menus can either have an action or a role. not both.
53
- ...(item.role ? { role: item.role } : { action: item.action || "" }),
54
- // default enabled to true unless explicitly set to false
55
- enabled: item.enabled === false ? false : true,
56
- checked: Boolean(item.checked),
57
- hidden: Boolean(item.hidden),
58
- tooltip: item.tooltip || undefined,
59
- accelerator: item.accelerator || undefined,
60
- ...(item.submenu
61
- ? { submenu: menuConfigWithDefaults(item.submenu) }
62
- : {}),
63
- };
64
- }
65
- });
66
- };