|
import { BrowserWindow } from "electron" |
|
import { differenceBy, intersectionBy } from "lodash" |
|
|
|
/** |
|
* WindowElm is the declarative representation of a window, similar to JSX elements in React. |
|
* It describes what the window should look like and how it should behave. |
|
*/ |
|
export type WindowElm = { |
|
id: string |
|
focused: boolean |
|
rect: { left: number; top: number; width: number; height: number } |
|
/** Arguments passed to BrowserWindow constructor */ |
|
args?: Electron.BrowserWindowConstructorOptions |
|
/** Called when the window is first created */ |
|
onCreate: (browserWindow: BrowserWindow) => void |
|
/** Called when the window is moved */ |
|
onMove: (browserWindow: BrowserWindow) => void |
|
/** Called when the window is resized */ |
|
onResize: (browserWindow: BrowserWindow) => void |
|
/** Called when the window gains focus */ |
|
onFocus: (browserWindow: BrowserWindow) => void |
|
/** Called when the system attempts to close the window */ |
|
onClose: (browserWindow: BrowserWindow, event: Electron.Event) => void |
|
} |
|
|
|
/** |
|
* WindowRef is the "reified" version of WindowElm - it includes both the element |
|
* and the actual BrowserWindow instance, similar to DOM nodes in React. |
|
*/ |
|
export type WindowRef = { |
|
elm: WindowElm |
|
browserWindow: BrowserWindow |
|
} |
|
|
|
/** |
|
* ElectronWindowManager manages the lifecycle of BrowserWindows based on declarative WindowElm descriptions. |
|
* It performs reconciliation similar to React's virtual DOM reconciliation. |
|
* This is analogous to ReactDOM - it renders declarative elements into actual browser windows. |
|
*/ |
|
export class ElectronWindowManager { |
|
refs: Map<string, WindowRef> = new Map() |
|
get windows(): WindowRef[] { |
|
return Array.from(this.refs.values()) |
|
} |
|
|
|
render(nextElements: WindowElm[]) { |
|
const prevElements = Array.from(this.refs.values()).map((ref) => ref.elm) |
|
|
|
// Diff the elements to determine what changed |
|
const toCreate = differenceBy(nextElements, prevElements, (elm) => elm.id) |
|
const toDestroy = differenceBy(prevElements, nextElements, (elm) => elm.id) |
|
const toUpdate = intersectionBy(nextElements, prevElements, (elm) => elm.id) |
|
|
|
// Destroy windows that no longer exist |
|
for (const elm of toDestroy) { |
|
const ref = this.refs.get(elm.id) |
|
if (ref) { |
|
ref.browserWindow.destroy() |
|
this.refs.delete(elm.id) |
|
} |
|
} |
|
|
|
// Update existing windows |
|
for (const elm of toUpdate) { |
|
const ref = this.refs.get(elm.id) |
|
if (ref) { |
|
this.updateWindow(ref, elm) |
|
} |
|
} |
|
|
|
// Create new windows |
|
for (const elm of toCreate) { |
|
const ref = this.createWindow(elm) |
|
this.refs.set(elm.id, ref) |
|
} |
|
} |
|
|
|
/** |
|
* Create a new window from an element |
|
*/ |
|
private createWindow(elm: WindowElm): WindowRef { |
|
const { rect, args = {} } = elm |
|
|
|
const browserWindow = new BrowserWindow({ |
|
...args, |
|
...rect, |
|
}) |
|
|
|
// Set up event listeners |
|
const moveHandler = () => { |
|
elm.onMove(browserWindow) |
|
} |
|
const resizeHandler = () => { |
|
elm.onResize(browserWindow) |
|
} |
|
const focusHandler = () => { |
|
elm.onFocus(browserWindow) |
|
} |
|
const closeHandler = (event: Electron.Event) => { |
|
elm.onClose(browserWindow, event) |
|
} |
|
|
|
browserWindow.on("move", moveHandler) |
|
browserWindow.on("resize", resizeHandler) |
|
browserWindow.on("focus", focusHandler) |
|
browserWindow.on("close", closeHandler) |
|
|
|
// Call onCreate lifecycle hook |
|
elm.onCreate(browserWindow) |
|
|
|
return { elm, browserWindow } |
|
} |
|
|
|
/** |
|
* Update an existing window with new element properties |
|
*/ |
|
private updateWindow(ref: WindowRef, nextElm: WindowElm) { |
|
const prevElm = ref.elm |
|
const { browserWindow } = ref |
|
|
|
// Update focus |
|
if (nextElm.focused && !browserWindow.isFocused()) { |
|
browserWindow.focus() |
|
} |
|
|
|
// Update rect |
|
if (prevElm.rect !== nextElm.rect) { |
|
const prevRect = prevElm.rect |
|
const nextRect = nextElm.rect |
|
|
|
if (prevRect.left !== nextRect.left || prevRect.top !== nextRect.top) { |
|
browserWindow.setPosition(nextRect.left, nextRect.top, false) |
|
} |
|
|
|
if (prevRect.height !== nextRect.height || prevRect.width !== nextRect.width) { |
|
browserWindow.setSize(nextRect.width, nextRect.height, false) |
|
} |
|
} |
|
|
|
// Update the element reference |
|
ref.elm = nextElm |
|
} |
|
|
|
destroy() { |
|
for (const ref of this.refs.values()) { |
|
ref.browserWindow.destroy() |
|
} |
|
this.refs.clear() |
|
} |
|
} |