electrobun 1.14.4 → 1.15.0-beta.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/README.md ADDED
@@ -0,0 +1,120 @@
1
+ <p align="center">
2
+ <a href="https://electrobun.dev"><img src="https://github.com/blackboardsh/electrobun/assets/75102186/8799b522-0507-45e9-86e3-c3cfded1aa7c" alt="Logo" height=170></a>
3
+ </p>
4
+
5
+ <h1 align="center">Electrobun</h1>
6
+
7
+ <div align="center">
8
+ Get started with a template <br />
9
+ <code><strong>npx electrobun init</strong></code>
10
+ </div>
11
+
12
+
13
+
14
+ ## What is Electrobun?
15
+
16
+ Electrobun aims to be a complete **solution-in-a-box** for building, updating, and shipping ultra fast, tiny, and cross-platform desktop applications written in Typescript.
17
+ Under the hood it uses <a href="https://bun.sh">bun</a> to execute the main process and to bundle webview typescript, and has native bindings written in <a href="https://ziglang.org/">zig</a>.
18
+
19
+ Visit <a href="https://blackboard.sh/electrobun/">https://blackboard.sh/electrobun/</a> to see api documentation, guides, and more.
20
+
21
+ **Project Goals**
22
+
23
+ - Write typescript for the main process and webviews without having to think about it.
24
+ - Isolation between main and webview processes with fast, typed, easy to implement RPC between them.
25
+ - Small self-extracting app bundles ~12MB (when using system webview, most of this is the bun runtime)
26
+ - Even smaller app updates as small as 14KB (using bsdiff it only downloads tiny patches between versions)
27
+ - Provide everything you need in one tightly integrated workflow to start writing code in 5 minutes and distribute in 10.
28
+
29
+ ## Apps Built with Electrobun
30
+ - [Audio TTS](https://github.com/blackboardsh/audio-tts) - desktop text-to-speech app using Qwen3-TTS for voice design, cloning, and generation
31
+ - [Co(lab)](https://blackboard.sh/colab/) - a hybrid web browser + code editor for deep work
32
+ - [DOOM](https://github.com/blackboardsh/electrobun-doom) - DOOM implemented in 2 ways: bun -> (c doom -> bundled wgpu) and (full ts port bun -> bundled wgpu)
33
+
34
+ ### Video Demos
35
+
36
+ [![Audio TTS Demo](https://img.youtube.com/vi/Z4dNK1d6l6E/maxresdefault.jpg)](https://www.youtube.com/watch?v=Z4dNK1d6l6E)
37
+
38
+ [![Co(lab) Demo](https://img.youtube.com/vi/WWTCqGmE86w/maxresdefault.jpg)](https://www.youtube.com/watch?v=WWTCqGmE86w)
39
+
40
+ [![DOOM Demo](https://github.com/user-attachments/assets/6cc5f04a-6d97-4010-b65f-3f282d32590c)](https://x.com/YoavCodes/status/2028499038148903239?s=20)
41
+
42
+ ## Star History
43
+
44
+ [![Star History Chart](https://api.star-history.com/svg?repos=blackboardsh/electrobun&type=date&legend=top-left&cache=2)](https://www.star-history.com/#blackboardsh/electrobun&type=date&legend=top-left)
45
+
46
+ ## Contributing
47
+ Ways to get involved:
48
+
49
+ - Follow us on X for updates <a href="https://twitter.com/BlackboardTech">@BlackboardTech</a> or <a href="https://bsky.app/profile/yoav.codes">@yoav.codes</a>
50
+ - Join the conversation on <a href="https://discord.gg/ueKE4tjaCE">Discord</a>
51
+ - Create and participate in Github issues and discussions
52
+ - Let me know what you're building with Electrobun
53
+
54
+ ## Development Setup
55
+ Building apps with Electrobun is as easy as updating your package.json dependencies with `npm add electrobun` or try one of our templates via `npx electrobun init`.
56
+
57
+ **This section is for building Electrobun from source locally in order to contribute fixes to it.**
58
+
59
+ ### Prerequisites
60
+
61
+ **macOS:**
62
+ - Xcode command line tools
63
+ - cmake (install via homebrew: `brew install cmake`)
64
+
65
+ **Windows:**
66
+ - Visual Studio Build Tools or Visual Studio with C++ development tools
67
+ - cmake
68
+
69
+ **Linux:**
70
+ - build-essential package
71
+ - cmake
72
+ - webkit2gtk and GTK development packages
73
+
74
+ On Ubuntu/Debian based distros: `sudo apt install build-essential cmake pkg-config libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev`
75
+
76
+ ### First-time Setup
77
+
78
+ ```bash
79
+ git clone --recurse-submodules https://github.com/blackboardsh/electrobun.git
80
+ cd electrobun/package
81
+ bun install
82
+ bun dev:clean
83
+ ```
84
+
85
+ ### Development Workflow
86
+
87
+ ```bash
88
+ # All commands are run from the /package directory
89
+ cd electrobun/package
90
+
91
+ # After making changes to source code
92
+ bun dev
93
+
94
+ # If you only changed kitchen sink code (not electrobun source)
95
+ bun dev:rerun
96
+
97
+ # If you need a completely fresh start
98
+ bun dev:clean
99
+ ```
100
+
101
+ ### Additional Commands
102
+
103
+ All commands are run from the `/package` directory:
104
+
105
+ - `bun dev:canary` - Build and run kitchen sink in canary mode
106
+ - `bun build:dev` - Build electrobun in development mode
107
+ - `bun build:release` - Build electrobun in release mode
108
+
109
+ ### Debugging
110
+
111
+ **macOS:** Use `lldb <path-to-bundle>/Contents/MacOS/launcher` and then `run` to debug release builds
112
+
113
+ ## Platform Support
114
+
115
+ | OS | Status |
116
+ |---|---|
117
+ | macOS 14+ | Official |
118
+ | Windows 11+ | Official |
119
+ | Ubuntu 22.04+ | Official |
120
+ | Other Linux distros (gtk3, webkit2gtk-4.1) | Community |
package/bun.lock CHANGED
@@ -5,10 +5,12 @@
5
5
  "": {
6
6
  "name": "electrobun",
7
7
  "dependencies": {
8
+ "@babylonjs/core": "^7.45.0",
8
9
  "@types/bun": "^1.3.8",
9
10
  "archiver": "^7.0.1",
10
11
  "png-to-ico": "^2.1.8",
11
12
  "rcedit": "^4.0.1",
13
+ "three": "^0.165.0",
12
14
  },
13
15
  "devDependencies": {
14
16
  "@types/archiver": "^6.0.3",
@@ -17,6 +19,8 @@
17
19
  },
18
20
  },
19
21
  "packages": {
22
+ "@babylonjs/core": ["@babylonjs/core@7.54.3", "", {}, "sha512-P5ncXVd8GEUJLhwloP9V0oVwQYIrvZztguVeLlvd5Rx+9aQnenKjpV8auJ6SRsUlAmNZU4pFTKzwF6o2EUfhAw=="],
23
+
20
24
  "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
21
25
 
22
26
  "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@1.1.1", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ=="],
@@ -169,6 +173,8 @@
169
173
 
170
174
  "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
171
175
 
176
+ "three": ["three@0.165.0", "", {}, "sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA=="],
177
+
172
178
  "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
173
179
 
174
180
  "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
@@ -11,6 +11,7 @@ import {
11
11
  type WebviewTagElement,
12
12
  type WebviewEventTypes,
13
13
  } from "./webviewtag";
14
+ import { type WgpuTagElement, type WgpuEventTypes } from "./wgputag";
14
15
  import "./global.d.ts";
15
16
 
16
17
  const WEBVIEW_ID = window.__electrobunWebviewId;
@@ -175,6 +176,8 @@ export {
175
176
  Electroview,
176
177
  type WebviewTagElement,
177
178
  type WebviewEventTypes,
179
+ type WgpuTagElement,
180
+ type WgpuEventTypes,
178
181
  };
179
182
 
180
183
  const Electrobun = {
@@ -0,0 +1,48 @@
1
+ import "./global.d.ts";
2
+
3
+ type WgpuEventTypes = "ready";
4
+
5
+ /**
6
+ * Interface representing an <electrobun-wgpu> custom element.
7
+ * Use this to properly type wgpu elements obtained via querySelector.
8
+ *
9
+ * @example
10
+ * const wgpu = document.querySelector('electrobun-wgpu') as WgpuTagElement;
11
+ * wgpu.toggleTransparent(true);
12
+ */
13
+ interface WgpuTagElement extends HTMLElement {
14
+ // Properties
15
+ wgpuViewId?: number;
16
+ transparent: boolean;
17
+ passthroughEnabled: boolean;
18
+ hidden: boolean;
19
+
20
+ // Visibility and interaction
21
+ toggleTransparent(transparent?: boolean): void;
22
+ togglePassthrough(enablePassthrough?: boolean): void;
23
+ toggleHidden(hidden?: boolean): void;
24
+
25
+ // Dimension sync
26
+ syncDimensions(force?: boolean): void;
27
+
28
+ // Debug helper
29
+ runTest(): void;
30
+
31
+ // Mask management
32
+ addMaskSelector(selector: string): void;
33
+ removeMaskSelector(selector: string): void;
34
+
35
+ // Events - listener receives a CustomEvent with detail property
36
+ on(event: WgpuEventTypes, listener: (event: CustomEvent) => void): void;
37
+ off(event: WgpuEventTypes, listener: (event: CustomEvent) => void): void;
38
+ emit(event: WgpuEventTypes, detail: unknown): void;
39
+ }
40
+
41
+ // Augment global types so querySelector('electrobun-wgpu') returns WgpuTagElement
42
+ declare global {
43
+ interface HTMLElementTagNameMap {
44
+ "electrobun-wgpu": WgpuTagElement;
45
+ }
46
+ }
47
+
48
+ export { type WgpuTagElement, type WgpuEventTypes };
@@ -139,6 +139,16 @@ export interface ElectrobunConfig {
139
139
  */
140
140
  cefVersion?: string;
141
141
 
142
+ /**
143
+ * Override the Dawn (WebGPU) version.
144
+ * Format: semver string (e.g., "0.2.3") or tag (e.g., "v0.2.3-beta.0")
145
+ *
146
+ * This downloads the specified electrobun-dawn release and uses it
147
+ * instead of the latest release.
148
+ * @default Uses the latest electrobun-dawn release
149
+ */
150
+ wgpuVersion?: string;
151
+
142
152
  /**
143
153
  * Override the Bun runtime version.
144
154
  * Format: semver string (e.g., "1.4.2")
@@ -149,6 +159,14 @@ export interface ElectrobunConfig {
149
159
  */
150
160
  bunVersion?: string;
151
161
 
162
+ /**
163
+ * Locales to include in the ICU data file (Linux/Windows only).
164
+ * Set to '*' to include all locales, or specify a subset like ['en', 'de']
165
+ * to reduce app size. Has no effect on macOS (uses system ICU).
166
+ * @default '*'
167
+ */
168
+ locales?: string[] | "*";
169
+
152
170
  /**
153
171
  * macOS-specific build configuration
154
172
  */
@@ -171,6 +189,12 @@ export interface ElectrobunConfig {
171
189
  */
172
190
  bundleCEF?: boolean;
173
191
 
192
+ /**
193
+ * Bundle Dawn (WebGPU) for GPU-native rendering
194
+ * @default false
195
+ */
196
+ bundleWGPU?: boolean;
197
+
174
198
  /**
175
199
  * Default renderer for webviews when not explicitly specified
176
200
  * @default 'native'
@@ -214,6 +238,12 @@ export interface ElectrobunConfig {
214
238
  */
215
239
  bundleCEF?: boolean;
216
240
 
241
+ /**
242
+ * Bundle Dawn (WebGPU) for GPU-native rendering
243
+ * @default false
244
+ */
245
+ bundleWGPU?: boolean;
246
+
217
247
  /**
218
248
  * Default renderer for webviews when not explicitly specified
219
249
  * @default 'native'
@@ -255,6 +285,12 @@ export interface ElectrobunConfig {
255
285
  */
256
286
  bundleCEF?: boolean;
257
287
 
288
+ /**
289
+ * Bundle Dawn (WebGPU) for GPU-native rendering
290
+ * @default false
291
+ */
292
+ bundleWGPU?: boolean;
293
+
258
294
  /**
259
295
  * Default renderer for webviews when not explicitly specified
260
296
  * @default 'native'
@@ -5,11 +5,12 @@ import { type Pointer } from "bun:ffi";
5
5
  import { BuildConfig } from "./BuildConfig";
6
6
  import { quit } from "./Utils";
7
7
  import { type RPCWithTransport } from "../../shared/rpc.js";
8
+ import { getNextWindowId } from "./windowIds";
9
+ import { GpuWindowMap } from "./GpuWindow";
10
+ import { WGPUView } from "./WGPUView";
8
11
 
9
12
  const buildConfig = await BuildConfig.get();
10
13
 
11
- let nextWindowId = 1;
12
-
13
14
  export type WindowOptionsType<T = undefined> = {
14
15
  title: string;
15
16
  frame: {
@@ -72,16 +73,38 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
72
73
  }
73
74
  }
74
75
 
76
+ // Clean up all WGPU views associated with this window
77
+ const wgpuViews = WGPUView.getAll().filter(v => v.windowId === windowId);
78
+ for (const view of wgpuViews) {
79
+ try {
80
+ // If ptr is null, the view was already cleaned up by the renderer or native cleanup
81
+ if (view.ptr === null) {
82
+ // Already cleaned up, skip
83
+ } else {
84
+ // Programmatic close path - remove the view
85
+ view.remove();
86
+ }
87
+ } catch (e) {
88
+ console.error(`Error cleaning up WGPU view ${view.id}:`, e);
89
+ // If remove() failed, at least mark it as cleaned up
90
+ view.ptr = null as any;
91
+ }
92
+ }
93
+
75
94
  const exitOnLastWindowClosed =
76
95
  buildConfig.runtime?.exitOnLastWindowClosed ?? true;
77
96
 
78
- if (exitOnLastWindowClosed && Object.keys(BrowserWindowMap).length === 0) {
97
+ if (
98
+ exitOnLastWindowClosed &&
99
+ Object.keys(BrowserWindowMap).length === 0 &&
100
+ Object.keys(GpuWindowMap).length === 0
101
+ ) {
79
102
  quit();
80
103
  }
81
104
  });
82
105
 
83
106
  export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
84
- id: number = nextWindowId++;
107
+ id: number = getNextWindowId();
85
108
  ptr!: Pointer;
86
109
  title: string = "Electrobun";
87
110
  state: "creating" | "created" = "creating";
@@ -0,0 +1,248 @@
1
+ import { ffi } from "../proc/native";
2
+ import electrobunEventEmitter from "../events/eventEmitter";
3
+ import { type Pointer } from "bun:ffi";
4
+ import { WGPUView } from "./WGPUView";
5
+ import { getNextWindowId } from "./windowIds";
6
+
7
+
8
+ export type GpuWindowOptionsType = {
9
+ title: string;
10
+ frame: {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ };
16
+ styleMask?: {};
17
+ titleBarStyle: "hidden" | "hiddenInset" | "default";
18
+ transparent: boolean;
19
+ };
20
+
21
+ const defaultOptions: GpuWindowOptionsType = {
22
+ title: "Electrobun",
23
+ frame: {
24
+ x: 0,
25
+ y: 0,
26
+ width: 800,
27
+ height: 600,
28
+ },
29
+ titleBarStyle: "default",
30
+ transparent: false,
31
+ };
32
+
33
+ export const GpuWindowMap: {
34
+ [id: number]: GpuWindow;
35
+ } = {};
36
+
37
+ // Clean up the window map when a window closes and optionally quit the app
38
+ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
39
+ const windowId = event.data.id;
40
+ delete GpuWindowMap[windowId];
41
+
42
+ // Clean up all WGPU views associated with this window
43
+ for (const view of WGPUView.getAll()) {
44
+ if (view.windowId === windowId) {
45
+ view.remove();
46
+ }
47
+ }
48
+
49
+ });
50
+
51
+ export class GpuWindow {
52
+ id: number = getNextWindowId();
53
+ ptr!: Pointer;
54
+ title: string = "Electrobun";
55
+ state: "creating" | "created" = "creating";
56
+ transparent: boolean = false;
57
+ frame: {
58
+ x: number;
59
+ y: number;
60
+ width: number;
61
+ height: number;
62
+ } = {
63
+ x: 0,
64
+ y: 0,
65
+ width: 800,
66
+ height: 600,
67
+ };
68
+ wgpuViewId!: number;
69
+
70
+ constructor(options: Partial<GpuWindowOptionsType> = defaultOptions) {
71
+ this.title = options.title || "New Window";
72
+ this.frame = options.frame
73
+ ? { ...defaultOptions.frame, ...options.frame }
74
+ : { ...defaultOptions.frame };
75
+ this.transparent = options.transparent ?? false;
76
+
77
+ this.init(options);
78
+ }
79
+
80
+ init({
81
+ styleMask,
82
+ titleBarStyle,
83
+ transparent,
84
+ }: Partial<GpuWindowOptionsType>) {
85
+ this.ptr = ffi.request.createWindow({
86
+ id: this.id,
87
+ title: this.title,
88
+ url: "",
89
+ frame: {
90
+ width: this.frame.width,
91
+ height: this.frame.height,
92
+ x: this.frame.x,
93
+ y: this.frame.y,
94
+ },
95
+ styleMask: {
96
+ Borderless: false,
97
+ Titled: true,
98
+ Closable: true,
99
+ Miniaturizable: true,
100
+ Resizable: true,
101
+ UnifiedTitleAndToolbar: false,
102
+ FullScreen: false,
103
+ FullSizeContentView: false,
104
+ UtilityWindow: false,
105
+ DocModalWindow: false,
106
+ NonactivatingPanel: false,
107
+ HUDWindow: false,
108
+ ...(styleMask || {}),
109
+ // hiddenInset: transparent titlebar with inset native controls
110
+ ...(titleBarStyle === "hiddenInset"
111
+ ? {
112
+ Titled: true,
113
+ FullSizeContentView: true,
114
+ }
115
+ : {}),
116
+ // hidden: no titlebar, no native controls (for fully custom chrome)
117
+ ...(titleBarStyle === "hidden"
118
+ ? {
119
+ Titled: false,
120
+ FullSizeContentView: true,
121
+ }
122
+ : {}),
123
+ },
124
+ titleBarStyle: titleBarStyle || "default",
125
+ transparent: transparent ?? false,
126
+ }) as Pointer;
127
+
128
+ GpuWindowMap[this.id] = this;
129
+
130
+ const wgpuView = new WGPUView({
131
+ frame: {
132
+ x: 0,
133
+ y: 0,
134
+ width: this.frame.width,
135
+ height: this.frame.height,
136
+ },
137
+ windowId: this.id,
138
+ autoResize: true,
139
+ startTransparent: false,
140
+ startPassthrough: false,
141
+ });
142
+
143
+ this.wgpuViewId = wgpuView.id;
144
+ }
145
+
146
+ get wgpuView() {
147
+ return WGPUView.getById(this.wgpuViewId) as WGPUView;
148
+ }
149
+
150
+ static getById(id: number) {
151
+ return GpuWindowMap[id];
152
+ }
153
+
154
+ setTitle(title: string) {
155
+ this.title = title;
156
+ return ffi.request.setTitle({ winId: this.id, title });
157
+ }
158
+
159
+ close() {
160
+ return ffi.request.closeWindow({ winId: this.id });
161
+ }
162
+
163
+ focus() {
164
+ return ffi.request.focusWindow({ winId: this.id });
165
+ }
166
+
167
+ show() {
168
+ return ffi.request.focusWindow({ winId: this.id });
169
+ }
170
+
171
+ minimize() {
172
+ return ffi.request.minimizeWindow({ winId: this.id });
173
+ }
174
+
175
+ unminimize() {
176
+ return ffi.request.restoreWindow({ winId: this.id });
177
+ }
178
+
179
+ isMinimized(): boolean {
180
+ return ffi.request.isWindowMinimized({ winId: this.id });
181
+ }
182
+
183
+ maximize() {
184
+ return ffi.request.maximizeWindow({ winId: this.id });
185
+ }
186
+
187
+ unmaximize() {
188
+ return ffi.request.unmaximizeWindow({ winId: this.id });
189
+ }
190
+
191
+ isMaximized(): boolean {
192
+ return ffi.request.isWindowMaximized({ winId: this.id });
193
+ }
194
+
195
+ setFullScreen(fullScreen: boolean) {
196
+ return ffi.request.setWindowFullScreen({ winId: this.id, fullScreen });
197
+ }
198
+
199
+ isFullScreen(): boolean {
200
+ return ffi.request.isWindowFullScreen({ winId: this.id });
201
+ }
202
+
203
+ setAlwaysOnTop(alwaysOnTop: boolean) {
204
+ return ffi.request.setWindowAlwaysOnTop({ winId: this.id, alwaysOnTop });
205
+ }
206
+
207
+ isAlwaysOnTop(): boolean {
208
+ return ffi.request.isWindowAlwaysOnTop({ winId: this.id });
209
+ }
210
+
211
+ setPosition(x: number, y: number) {
212
+ this.frame.x = x;
213
+ this.frame.y = y;
214
+ return ffi.request.setWindowPosition({ winId: this.id, x, y });
215
+ }
216
+
217
+ setSize(width: number, height: number) {
218
+ this.frame.width = width;
219
+ this.frame.height = height;
220
+ return ffi.request.setWindowSize({ winId: this.id, width, height });
221
+ }
222
+
223
+ setFrame(x: number, y: number, width: number, height: number) {
224
+ this.frame = { x, y, width, height };
225
+ return ffi.request.setWindowFrame({ winId: this.id, x, y, width, height });
226
+ }
227
+
228
+ getFrame(): { x: number; y: number; width: number; height: number } {
229
+ const frame = ffi.request.getWindowFrame({ winId: this.id });
230
+ this.frame = frame;
231
+ return frame;
232
+ }
233
+
234
+ getPosition(): { x: number; y: number } {
235
+ const frame = this.getFrame();
236
+ return { x: frame.x, y: frame.y };
237
+ }
238
+
239
+ getSize(): { width: number; height: number } {
240
+ const frame = this.getFrame();
241
+ return { width: frame.width, height: frame.height };
242
+ }
243
+
244
+ on(name: string, handler: (event: unknown) => void) {
245
+ const specificName = `${name}-${this.id}`;
246
+ electrobunEventEmitter.on(specificName, handler);
247
+ }
248
+ }