2023-12-13 21:01:03 +00:00
|
|
|
import { BrowserWindow, AutoResizeOptions, Rectangle, WebContentsView, WebPreferences, WebContents } from 'electron/main';
|
Implement initial, experimental BrowserView API
Right now, `<webview>` is the only way to embed additional content in a
`BrowserWindow`. Unfortunately `<webview>` suffers from a [number of
problems](https://github.com/electron/electron/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aopen%20label%3Awebview%20).
To make matters worse, many of these are upstream Chromium bugs instead
of Electron-specific bugs.
For us at [Figma](https://www.figma.com), the main issue is very slow
performance.
Despite the upstream improvements to `<webview>` through the OOPIF work, it is
probable that there will continue to be `<webview>`-specific bugs in the
future.
Therefore, this introduces a `<webview>` alternative to called `BrowserView`,
which...
- is a thin wrapper around `api::WebContents` (so bugs in `BrowserView` will
likely also be bugs in `BrowserWindow` web contents)
- is instantiated in the main process like `BrowserWindow` (and unlike
`<webview>`, which lives in the DOM of a `BrowserWindow` web contents)
- needs to be added to a `BrowserWindow` to display something on the screen
This implements the most basic API. The API is expected to evolve and change in
the near future and has consequently been marked as experimental. Please do not
use this API in production unless you are prepared to deal with breaking
changes.
In the future, we will want to change the API to support multiple
`BrowserView`s per window. We will also want to consider z-ordering
auto-resizing, and possibly even nested views.
2017-04-11 17:47:30 +00:00
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
const v8Util = process._linkedBinding('electron_common_v8_util');
|
|
|
|
|
|
|
|
export default class BrowserView {
|
|
|
|
#webContentsView: WebContentsView;
|
2024-07-08 10:13:53 +00:00
|
|
|
#ownerWindow: BrowserWindow | null = null;
|
|
|
|
|
|
|
|
#destroyListener: ((e: any) => void) | null = null;
|
2023-12-13 21:01:03 +00:00
|
|
|
|
|
|
|
// AutoResize state
|
|
|
|
#resizeListener: ((...args: any[]) => void) | null = null;
|
|
|
|
#lastWindowSize: {width: number, height: number} = { width: 0, height: 0 };
|
|
|
|
#autoResizeFlags: AutoResizeOptions = {};
|
|
|
|
|
|
|
|
constructor (options: {webPreferences: WebPreferences, webContents?: WebContents} = { webPreferences: {} }) {
|
|
|
|
const { webPreferences = {}, webContents } = options;
|
|
|
|
if (webContents) {
|
|
|
|
v8Util.setHiddenValue(webPreferences, 'webContents', webContents);
|
|
|
|
}
|
|
|
|
webPreferences.type = 'browserView';
|
|
|
|
this.#webContentsView = new WebContentsView({ webPreferences });
|
2024-07-08 10:13:53 +00:00
|
|
|
|
|
|
|
this.#destroyListener = this.#onDestroy.bind(this);
|
|
|
|
this.#webContentsView.webContents.once('destroyed', this.#destroyListener);
|
2023-12-13 21:01:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
get webContents () {
|
|
|
|
return this.#webContentsView.webContents;
|
|
|
|
}
|
|
|
|
|
|
|
|
setBounds (bounds: Rectangle) {
|
|
|
|
this.#webContentsView.setBounds(bounds);
|
|
|
|
this.#autoHorizontalProportion = null;
|
|
|
|
this.#autoVerticalProportion = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
getBounds () {
|
|
|
|
return this.#webContentsView.getBounds();
|
|
|
|
}
|
|
|
|
|
|
|
|
setAutoResize (options: AutoResizeOptions) {
|
2024-05-13 08:27:01 +00:00
|
|
|
if (options == null || typeof options !== 'object') {
|
|
|
|
throw new Error('Invalid auto resize options');
|
|
|
|
}
|
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
this.#autoResizeFlags = {
|
|
|
|
width: !!options.width,
|
|
|
|
height: !!options.height,
|
|
|
|
horizontal: !!options.horizontal,
|
|
|
|
vertical: !!options.vertical
|
|
|
|
};
|
2024-05-13 08:27:01 +00:00
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
this.#autoHorizontalProportion = null;
|
|
|
|
this.#autoVerticalProportion = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
setBackgroundColor (color: string) {
|
|
|
|
this.#webContentsView.setBackgroundColor(color);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Internal methods
|
|
|
|
get ownerWindow (): BrowserWindow | null {
|
2024-07-08 10:13:53 +00:00
|
|
|
return this.#ownerWindow;
|
2023-12-13 21:01:03 +00:00
|
|
|
}
|
|
|
|
|
2024-07-08 10:13:53 +00:00
|
|
|
// We can't rely solely on the webContents' owner window because
|
|
|
|
// a webContents can be closed by the user while the BrowserView
|
|
|
|
// remains alive and attached to a BrowserWindow.
|
2023-12-13 21:01:03 +00:00
|
|
|
set ownerWindow (w: BrowserWindow | null) {
|
2024-07-08 10:13:53 +00:00
|
|
|
if (this.#ownerWindow && this.#resizeListener) {
|
|
|
|
this.#ownerWindow.off('resize', this.#resizeListener);
|
2023-12-13 21:01:03 +00:00
|
|
|
this.#resizeListener = null;
|
|
|
|
}
|
2024-07-08 10:13:53 +00:00
|
|
|
|
|
|
|
if (this.webContents && !this.webContents.isDestroyed()) {
|
|
|
|
this.webContents._setOwnerWindow(w);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.#ownerWindow = w;
|
2023-12-13 21:01:03 +00:00
|
|
|
if (w) {
|
|
|
|
this.#lastWindowSize = w.getBounds();
|
|
|
|
w.on('resize', this.#resizeListener = this.#autoResize.bind(this));
|
2024-07-08 10:13:53 +00:00
|
|
|
w.on('closed', () => {
|
|
|
|
this.#ownerWindow = null;
|
|
|
|
this.#destroyListener = null;
|
|
|
|
});
|
2023-12-13 21:01:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-08 10:13:53 +00:00
|
|
|
#onDestroy () {
|
|
|
|
// Ensure that if #webContentsView's webContents is destroyed,
|
|
|
|
// the WebContentsView is removed from the view hierarchy.
|
|
|
|
this.#ownerWindow?.contentView.removeChildView(this.webContentsView);
|
|
|
|
}
|
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
#autoHorizontalProportion: {width: number, left: number} | null = null;
|
|
|
|
#autoVerticalProportion: {height: number, top: number} | null = null;
|
|
|
|
#autoResize () {
|
2024-05-13 08:27:01 +00:00
|
|
|
if (!this.ownerWindow) {
|
|
|
|
throw new Error('Electron bug: #autoResize called without owner window');
|
|
|
|
};
|
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
if (this.#autoResizeFlags.horizontal && this.#autoHorizontalProportion == null) {
|
|
|
|
const viewBounds = this.#webContentsView.getBounds();
|
|
|
|
this.#autoHorizontalProportion = {
|
|
|
|
width: this.#lastWindowSize.width / viewBounds.width,
|
|
|
|
left: this.#lastWindowSize.width / viewBounds.x
|
|
|
|
};
|
|
|
|
}
|
2024-05-13 08:27:01 +00:00
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
if (this.#autoResizeFlags.vertical && this.#autoVerticalProportion == null) {
|
|
|
|
const viewBounds = this.#webContentsView.getBounds();
|
|
|
|
this.#autoVerticalProportion = {
|
|
|
|
height: this.#lastWindowSize.height / viewBounds.height,
|
|
|
|
top: this.#lastWindowSize.height / viewBounds.y
|
|
|
|
};
|
|
|
|
}
|
2024-05-13 08:27:01 +00:00
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
const newBounds = this.ownerWindow.getBounds();
|
|
|
|
let widthDelta = newBounds.width - this.#lastWindowSize.width;
|
|
|
|
let heightDelta = newBounds.height - this.#lastWindowSize.height;
|
|
|
|
if (!this.#autoResizeFlags.width) widthDelta = 0;
|
|
|
|
if (!this.#autoResizeFlags.height) heightDelta = 0;
|
|
|
|
|
|
|
|
const newViewBounds = this.#webContentsView.getBounds();
|
|
|
|
if (widthDelta || heightDelta) {
|
|
|
|
this.#webContentsView.setBounds({
|
|
|
|
...newViewBounds,
|
|
|
|
width: newViewBounds.width + widthDelta,
|
|
|
|
height: newViewBounds.height + heightDelta
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.#autoHorizontalProportion) {
|
|
|
|
newViewBounds.width = newBounds.width / this.#autoHorizontalProportion.width;
|
|
|
|
newViewBounds.x = newBounds.width / this.#autoHorizontalProportion.left;
|
|
|
|
}
|
2024-05-13 08:27:01 +00:00
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
if (this.#autoVerticalProportion) {
|
|
|
|
newViewBounds.height = newBounds.height / this.#autoVerticalProportion.height;
|
|
|
|
newViewBounds.y = newBounds.y / this.#autoVerticalProportion.top;
|
|
|
|
}
|
2024-05-13 08:27:01 +00:00
|
|
|
|
2023-12-13 21:01:03 +00:00
|
|
|
if (this.#autoHorizontalProportion || this.#autoVerticalProportion) {
|
|
|
|
this.#webContentsView.setBounds(newViewBounds);
|
|
|
|
}
|
2024-09-09 12:22:30 +00:00
|
|
|
|
|
|
|
// Update #lastWindowSize value after browser windows resize
|
|
|
|
this.#lastWindowSize = {
|
|
|
|
width: newBounds.width,
|
|
|
|
height: newBounds.height
|
|
|
|
};
|
2023-12-13 21:01:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
get webContentsView () {
|
|
|
|
return this.#webContentsView;
|
|
|
|
}
|
|
|
|
}
|