electrobun 1.14.5-beta.0 → 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 CHANGED
@@ -29,6 +29,7 @@ Visit <a href="https://blackboard.sh/electrobun/">https://blackboard.sh/electrob
29
29
  ## Apps Built with Electrobun
30
30
  - [Audio TTS](https://github.com/blackboardsh/audio-tts) - desktop text-to-speech app using Qwen3-TTS for voice design, cloning, and generation
31
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)
32
33
 
33
34
  ### Video Demos
34
35
 
@@ -36,9 +37,11 @@ Visit <a href="https://blackboard.sh/electrobun/">https://blackboard.sh/electrob
36
37
 
37
38
  [![Co(lab) Demo](https://img.youtube.com/vi/WWTCqGmE86w/maxresdefault.jpg)](https://www.youtube.com/watch?v=WWTCqGmE86w)
38
39
 
40
+ [![DOOM Demo](https://github.com/user-attachments/assets/6cc5f04a-6d97-4010-b65f-3f282d32590c)](https://x.com/YoavCodes/status/2028499038148903239?s=20)
41
+
39
42
  ## Star History
40
43
 
41
- [![Star History Chart](https://api.star-history.com/svg?repos=blackboardsh/electrobun&type=date&legend=top-left&cache=1)](https://www.star-history.com/#blackboardsh/electrobun&type=date&legend=top-left)
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)
42
45
 
43
46
  ## Contributing
44
47
  Ways to get involved:
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
+ }
@@ -0,0 +1,137 @@
1
+ import { ffi } from "../proc/native";
2
+ import electrobunEventEmitter from "../events/eventEmitter";
3
+ import { type Pointer } from "bun:ffi";
4
+
5
+ const WGPUViewMap: {
6
+ [id: number]: WGPUView;
7
+ } = {};
8
+
9
+ let nextWGPUViewId = 1;
10
+
11
+ export type WGPUViewOptions = {
12
+ frame: {
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ };
18
+ autoResize: boolean;
19
+ windowId: number;
20
+ startTransparent: boolean;
21
+ startPassthrough: boolean;
22
+ };
23
+
24
+ const defaultOptions: Partial<WGPUViewOptions> = {
25
+ frame: {
26
+ x: 0,
27
+ y: 0,
28
+ width: 800,
29
+ height: 600,
30
+ },
31
+ autoResize: true,
32
+ startTransparent: false,
33
+ startPassthrough: false,
34
+ };
35
+
36
+ export class WGPUView {
37
+ id: number = nextWGPUViewId++;
38
+ ptr!: Pointer;
39
+ windowId!: number;
40
+ autoResize: boolean = true;
41
+ frame: {
42
+ x: number;
43
+ y: number;
44
+ width: number;
45
+ height: number;
46
+ } = {
47
+ x: 0,
48
+ y: 0,
49
+ width: 800,
50
+ height: 600,
51
+ };
52
+ startTransparent: boolean = false;
53
+ startPassthrough: boolean = false;
54
+
55
+ constructor(options: Partial<WGPUViewOptions> = defaultOptions) {
56
+ this.frame = {
57
+ x: options.frame?.x ?? defaultOptions.frame!.x,
58
+ y: options.frame?.y ?? defaultOptions.frame!.y,
59
+ width: options.frame?.width ?? defaultOptions.frame!.width,
60
+ height: options.frame?.height ?? defaultOptions.frame!.height,
61
+ };
62
+ this.windowId = options.windowId ?? 0;
63
+ this.autoResize = options.autoResize === false ? false : true;
64
+ this.startTransparent = options.startTransparent ?? false;
65
+ this.startPassthrough = options.startPassthrough ?? false;
66
+
67
+ WGPUViewMap[this.id] = this;
68
+ this.ptr = this.init() as Pointer;
69
+ }
70
+
71
+ init() {
72
+ return ffi.request.createWGPUView({
73
+ id: this.id,
74
+ windowId: this.windowId,
75
+ frame: {
76
+ width: this.frame.width,
77
+ height: this.frame.height,
78
+ x: this.frame.x,
79
+ y: this.frame.y,
80
+ },
81
+ autoResize: this.autoResize,
82
+ startTransparent: this.startTransparent,
83
+ startPassthrough: this.startPassthrough,
84
+ });
85
+ }
86
+
87
+ setFrame(x: number, y: number, width: number, height: number) {
88
+ this.frame = { x, y, width, height };
89
+ ffi.request.wgpuViewSetFrame({ id: this.id, x, y, width, height });
90
+ }
91
+
92
+ setTransparent(transparent: boolean) {
93
+ ffi.request.wgpuViewSetTransparent({ id: this.id, transparent });
94
+ }
95
+
96
+ setPassthrough(passthrough: boolean) {
97
+ ffi.request.wgpuViewSetPassthrough({ id: this.id, passthrough });
98
+ }
99
+
100
+ setHidden(hidden: boolean) {
101
+ ffi.request.wgpuViewSetHidden({ id: this.id, hidden });
102
+ }
103
+
104
+ on(name: "frame-updated", handler: (event: unknown) => void) {
105
+ const specificName = `${name}-${this.id}`;
106
+ electrobunEventEmitter.on(specificName, handler);
107
+ }
108
+
109
+ remove() {
110
+ // Check if already removed
111
+ if (this.ptr === null) {
112
+ return;
113
+ }
114
+
115
+ try {
116
+ ffi.request.wgpuViewRemove({ id: this.id });
117
+ } catch (e) {
118
+ console.error(`Error removing WGPU view ${this.id}:`, e);
119
+ }
120
+
121
+ delete WGPUViewMap[this.id];
122
+ // Clear the pointer to prevent any accidental access
123
+ this.ptr = null as any;
124
+ }
125
+
126
+ getNativeHandle() {
127
+ return ffi.request.wgpuViewGetNativeHandle({ id: this.id });
128
+ }
129
+
130
+ static getById(id: number) {
131
+ return WGPUViewMap[id];
132
+ }
133
+
134
+ static getAll() {
135
+ return Object.values(WGPUViewMap);
136
+ }
137
+ }
@@ -0,0 +1,5 @@
1
+ let nextWindowId = 1;
2
+
3
+ export function getNextWindowId() {
4
+ return nextWindowId++;
5
+ }
@@ -9,6 +9,7 @@ type ResizeData = {
9
9
  height: number;
10
10
  };
11
11
  type MoveData = { id: number; x: number; y: number };
12
+ type KeyData = { id: number; keyCode: number; modifiers: number; isRepeat: boolean };
12
13
 
13
14
  export default {
14
15
  close: (data: IdData) => new ElectrobunEvent<IdData, {}>("close", data),
@@ -16,4 +17,6 @@ export default {
16
17
  new ElectrobunEvent<ResizeData, {}>("resize", data),
17
18
  move: (data: MoveData) => new ElectrobunEvent<MoveData, {}>("move", data),
18
19
  focus: (data: IdData) => new ElectrobunEvent<IdData, {}>("focus", data),
20
+ keyDown: (data: KeyData) => new ElectrobunEvent<KeyData, {}>("keyDown", data),
21
+ keyUp: (data: KeyData) => new ElectrobunEvent<KeyData, {}>("keyUp", data),
19
22
  };