electrobun 0.0.3 → 0.0.4

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/dist/webview CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electrobun",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Build ultra fast, tiny, and cross-platform desktop apps with Typescript.",
5
5
  "license": "MIT",
6
6
  "author": "Blackboard Technologies Inc.",
@@ -45,6 +45,7 @@ type WebviewTagHandlers = RPCSchema<{
45
45
  width: number;
46
46
  height: number;
47
47
  };
48
+ masks: string;
48
49
  };
49
50
  webviewTagUpdateSrc: {
50
51
  id: number;
@@ -77,6 +78,10 @@ type WebviewTagHandlers = RPCSchema<{
77
78
  id: number;
78
79
  transparent: boolean;
79
80
  };
81
+ webviewTagToggleMirroring: {
82
+ id: number;
83
+ enable: boolean;
84
+ };
80
85
  webviewTagSetPassthrough: {
81
86
  id: number;
82
87
  enablePassthrough: boolean;
@@ -4,6 +4,8 @@ type WebviewEventTypes =
4
4
  | "did-commit-navigation"
5
5
  | "dom-ready";
6
6
 
7
+ type Rect = { x: number; y: number; width: number; height: number };
8
+
7
9
  const ConfigureWebviewTags = (
8
10
  enableWebviewTags: boolean,
9
11
  zigRpc: (params: any) => any,
@@ -24,6 +26,10 @@ const ConfigureWebviewTags = (
24
26
  zigRpc: any;
25
27
  syncRpc: any;
26
28
 
29
+ // querySelectors for elements that you want to appear
30
+ // in front of the webview.
31
+ maskSelectors: Set<string> = new Set();
32
+
27
33
  // observers
28
34
  resizeObserver?: ResizeObserver;
29
35
  // intersectionObserver?: IntersectionObserver;
@@ -39,12 +45,16 @@ const ConfigureWebviewTags = (
39
45
  height: 0,
40
46
  };
41
47
 
48
+ lastMasksJSON: string = "";
49
+ lastMasks: Rect[] = [];
50
+
42
51
  transparent: boolean = false;
43
52
  passthroughEnabled: boolean = false;
44
53
  hidden: boolean = false;
45
54
  delegateMode: boolean = false;
46
55
  hiddenMirrorMode: boolean = false;
47
56
  wasZeroRect: boolean = false;
57
+ isMirroring: boolean = false;
48
58
 
49
59
  partition: string | null = null;
50
60
 
@@ -59,6 +69,16 @@ const ConfigureWebviewTags = (
59
69
  });
60
70
  }
61
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
+
62
82
  initWebview() {
63
83
  const rect = this.getBoundingClientRect();
64
84
  this.lastRect = rect;
@@ -206,6 +226,10 @@ const ConfigureWebviewTags = (
206
226
  // know that they're chaning something in order to eliminate the lag that the
207
227
  // catch all loop will catch
208
228
  syncDimensions(force: boolean = false) {
229
+ if (!force && this.hidden) {
230
+ return;
231
+ }
232
+
209
233
  const rect = this.getBoundingClientRect();
210
234
  const { x, y, width, height } =
211
235
  this.adjustDimensionsForHiddenMirrorMode(rect);
@@ -220,19 +244,45 @@ const ConfigureWebviewTags = (
220
244
  return;
221
245
  }
222
246
 
247
+ const masks: Rect[] = [];
248
+ this.maskSelectors.forEach((selector) => {
249
+ const els = document.querySelectorAll(selector);
250
+
251
+ for (let i = 0; i < els.length; i++) {
252
+ const el = els[i];
253
+
254
+ if (el) {
255
+ const maskRect = el.getBoundingClientRect();
256
+
257
+ masks.push({
258
+ // reposition the bounding rect to be relative to the webview rect
259
+ // so objc can apply the mask correctly and handle the actual overlap
260
+ x: maskRect.x - x,
261
+ y: maskRect.y - y,
262
+ width: maskRect.width,
263
+ height: maskRect.height,
264
+ });
265
+ }
266
+ }
267
+ });
268
+
269
+ // store jsonStringified last masks value to compare
270
+ const masksJson = masks.length ? JSON.stringify(masks) : "";
271
+
223
272
  if (
224
273
  force ||
225
274
  lastRect.x !== x ||
226
275
  lastRect.y !== y ||
227
276
  lastRect.width !== width ||
228
- lastRect.height !== height
277
+ lastRect.height !== height ||
278
+ this.lastMasksJSON !== masksJson
229
279
  ) {
230
- // if we're not already in an accelerated loop then accelerate it
231
- if (!this.positionCheckLoopReset) {
232
- this.setPositionCheckLoop(true);
233
- }
280
+ // let it know we're still accelerating
281
+ this.setPositionCheckLoop(true);
234
282
 
235
283
  this.lastRect = rect;
284
+ this.lastMasks = masks;
285
+ this.lastMasksJSON = masksJson;
236
286
 
237
287
  this.zigRpc.send.webviewTagResize({
238
288
  id: this.webviewId,
@@ -242,6 +292,7 @@ const ConfigureWebviewTags = (
242
292
  x: x,
243
293
  y: y,
244
294
  },
295
+ masks: masksJson,
245
296
  });
246
297
  }
247
298
 
@@ -265,13 +316,12 @@ const ConfigureWebviewTags = (
265
316
  this.positionCheckLoopReset = undefined;
266
317
  }
267
318
 
268
- const delay = accelerate ? 100 : 400;
319
+ const delay = accelerate ? 0 : 300;
269
320
 
270
321
  if (accelerate) {
271
- clearTimeout(this.positionCheckLoopReset);
272
322
  this.positionCheckLoopReset = setTimeout(() => {
273
323
  this.setPositionCheckLoop(false);
274
- }, 600);
324
+ }, 2000);
275
325
  }
276
326
  // Note: Since there's not catch all way to listen for x/y changes
277
327
  // we have a 400ms interval to check
@@ -288,6 +338,59 @@ const ConfigureWebviewTags = (
288
338
  this.positionCheckLoop = setInterval(() => this.syncDimensions(), delay);
289
339
  }
290
340
 
341
+ // The global document mousemove will fire even when the mouse is over
342
+ // an OOPIF that's layered above this host webview. The two edge cases we
343
+ // solve for are:
344
+ // 1. dragging an element on the host over or dropping on the webview anchor
345
+ // 2. clicking on an element that's "layered over" the OOPIF visually but really uses a
346
+ // mask to make a section transparent. We want the underlying overlay UI to be
347
+ // interactive not the OOPIF.
348
+ //
349
+ // Solution: Have mirroing on by default.
350
+ // 1. mouse move events don't fire during drag. So the OOPIF remains non-interactive
351
+ // and effectively passes through to the host's anchor element underneath letting you
352
+ // react to drag events on the host as needed.
353
+ // 2. Detect when the mouse is moving over the anchor and turn mirroring off to make
354
+ // it interactive. Unless the mouse is over a masked area in which case we want to
355
+ // keep it non-interactive and pass through to the host "overylay UI".
356
+ handleDocumentMouseMove(e: MouseEvent) {
357
+ if (this.hidden) {
358
+ return;
359
+ }
360
+
361
+ const isInBounds =
362
+ e.clientX >= this.lastRect.x &&
363
+ e.clientX <= this.lastRect.x + this.lastRect.width &&
364
+ e.clientY >= this.lastRect.y &&
365
+ e.clientY <= this.lastRect.y + this.lastRect.height;
366
+
367
+ if (isInBounds) {
368
+ const isInMaskBounds = this.lastMasks.find((mask) => {
369
+ // we send relative x/y to objc but here we need the clientX/Y
370
+ // to compare against. consider doing the opposite or storing both.
371
+ const clientX = this.lastRect.x + mask.x;
372
+ const clientY = this.lastRect.y + mask.y;
373
+ const isInMaskBounds =
374
+ e.clientX >= clientX &&
375
+ e.clientX <= clientX + mask.width &&
376
+ e.clientY <= clientY + mask.height &&
377
+ e.clientY >= clientY;
378
+
379
+ return isInMaskBounds;
380
+ });
381
+ if (isInMaskBounds) {
382
+ this.startMirroring();
383
+ } else {
384
+ this.stopMirroring();
385
+ }
386
+ } else {
387
+ this.startMirroring();
388
+ }
389
+ }
390
+
391
+ boundhandleDocumentMouseMove = (e: MouseEvent) =>
392
+ this.handleDocumentMouseMove(e);
393
+
291
394
  connectedCallback() {
292
395
  this.setPositionCheckLoop();
293
396
 
@@ -300,8 +403,14 @@ const ConfigureWebviewTags = (
300
403
  // we still need to send it to objc to calculate from its bottom left position
301
404
  // otherwise it'll move around unexpectedly.
302
405
  window.addEventListener("resize", this.boundForceSyncDimensions);
303
-
304
406
  window.addEventListener("scroll", this.boundSyncDimensions);
407
+ // Note: mousemove won't fire during a drag so we get that behaviour
408
+ // for free without doing calculations.
409
+ document.addEventListener(
410
+ "mousemove",
411
+ this.boundhandleDocumentMouseMove,
412
+ true
413
+ );
305
414
 
306
415
  // todo: For chromium webviews (windows native or chromium bundled)
307
416
  // should be able to use performanceObservers on layout-shift to
@@ -318,6 +427,10 @@ const ConfigureWebviewTags = (
318
427
  // this.mutationObserver?.disconnect();
319
428
  window.removeEventListener("resize", this.boundForceSyncDimensions);
320
429
  window.removeEventListener("scroll", this.boundSyncDimensions);
430
+ document.removeEventListener(
431
+ "mousemove",
432
+ this.boundhandleDocumentMouseMove
433
+ );
321
434
  this.zigRpc.send.webviewTagRemove({ id: this.webviewId });
322
435
  }
323
436
 
@@ -407,9 +520,8 @@ const ConfigureWebviewTags = (
407
520
  DEFAULT_FRAME_RATE = Math.round(1000 / 30); // 30fps
408
521
  streamScreenInterval?: Timer;
409
522
 
410
- // todo: add a flag to disable automirroring since it's cpu intensive and
411
- // developers may want to disable it in specific cases
412
- startMirroring(frameRate: number = this.DEFAULT_FRAME_RATE) {
523
+ // NOTE: This is very cpu intensive, Prefer startMirroring where possible
524
+ startMirroringToDom(frameRate: number = this.DEFAULT_FRAME_RATE) {
413
525
  if (this.streamScreenInterval) {
414
526
  clearInterval(this.streamScreenInterval);
415
527
  }
@@ -419,13 +531,33 @@ const ConfigureWebviewTags = (
419
531
  }, frameRate);
420
532
  }
421
533
 
422
- stopMirroring() {
534
+ stopMirroringToDom() {
423
535
  if (this.streamScreenInterval) {
424
536
  clearInterval(this.streamScreenInterval);
425
537
  this.streamScreenInterval = undefined;
426
538
  }
427
539
  }
428
540
 
541
+ startMirroring() {
542
+ if (this.isMirroring === false) {
543
+ this.isMirroring = true;
544
+ this.zigRpc.send.webviewTagToggleMirroring({
545
+ id: this.webviewId,
546
+ enable: true,
547
+ });
548
+ }
549
+ }
550
+
551
+ stopMirroring() {
552
+ if (this.isMirroring === true) {
553
+ this.isMirroring = false;
554
+ this.zigRpc.send.webviewTagToggleMirroring({
555
+ id: this.webviewId,
556
+ enable: false,
557
+ });
558
+ }
559
+ }
560
+
429
561
  clearScreenImage() {
430
562
  this.style.backgroundImage = "";
431
563
  }
@@ -503,11 +635,11 @@ const ConfigureWebviewTags = (
503
635
  this.syncScreenshot(() => {
504
636
  this.delegateMode = true;
505
637
  this.toggleTransparent(true, true);
506
- this.startMirroring();
638
+ this.startMirroringToDom();
507
639
  });
508
640
  } else {
509
641
  this.delegateMode = false;
510
- this.stopMirroring();
642
+ this.stopMirroringToDom();
511
643
  this.toggleTransparent(this.transparent);
512
644
  this.tryClearScreenImage();
513
645
  }
@@ -527,10 +659,10 @@ const ConfigureWebviewTags = (
527
659
  this.hiddenMirrorMode = true;
528
660
  this.toggleHidden(true, true);
529
661
  this.togglePassthrough(true, true);
530
- this.startMirroring();
662
+ this.startMirroringToDom();
531
663
  });
532
664
  } else {
533
- this.stopMirroring();
665
+ this.stopMirroringToDom();
534
666
  this.toggleHidden(this.hidden);
535
667
  this.togglePassthrough(this.passthroughEnabled);
536
668
  this.tryClearScreenImage();
@@ -32,6 +32,7 @@ type BrowserViewOptions<T = undefined> = {
32
32
  rpc: T;
33
33
  syncRpc: { [method: string]: (params: any) => any };
34
34
  hostWebviewId: number;
35
+ autoResize: boolean;
35
36
  };
36
37
 
37
38
  interface ElectrobunWebviewRPCSChema {
@@ -68,6 +69,7 @@ const internalSyncRpcHandlers = {
68
69
  partition,
69
70
  frame,
70
71
  hostWebviewId,
72
+ autoResize: false,
71
73
  });
72
74
 
73
75
  // Note: we have to give it a couple of ticks to fully create the browserview
@@ -104,6 +106,7 @@ export class BrowserView<T> {
104
106
  html: string | null = null;
105
107
  preload: string | null = null;
106
108
  partition: string | null = null;
109
+ autoResize: boolean = true;
107
110
  frame: {
108
111
  x: number;
109
112
  y: number;
@@ -135,6 +138,7 @@ export class BrowserView<T> {
135
138
  // file exists first
136
139
  this.pipePrefix = `/private/tmp/electrobun_ipc_pipe_${hash}_${randomId}_${this.id}`;
137
140
  this.hostWebviewId = options.hostWebviewId;
141
+ this.autoResize = options.autoResize === false ? false : true;
138
142
 
139
143
  this.init();
140
144
  }
@@ -158,7 +162,7 @@ export class BrowserView<T> {
158
162
  x: this.frame.x,
159
163
  y: this.frame.y,
160
164
  },
161
- // autoResize: true,
165
+ autoResize: this.autoResize,
162
166
  });
163
167
 
164
168
  this.createStreams();
@@ -192,6 +192,7 @@ type ZigHandlers = RPCSchema<{
192
192
  width: number;
193
193
  height: number;
194
194
  };
195
+ autoResize: boolean;
195
196
  };
196
197
  response: void;
197
198
  };
Binary file
Binary file