Minimal WebGPU rendering example
This example opens a window and clears it to a solid color, proving the whole WebGPU pipeline is wired up:
```ts
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("no WebGPU adapter available");
const device = await adapter.requestDevice();
const win = new Deno.BrowserWindow({
title: "WebGPU",
width: 640,
height: 480,
});
const surface = win.getNativeWindow();
const format = navigator.gpu.getPreferredCanvasFormat();
const context = surface.getContext("webgpu");
context.configure({ device, format, alphaMode: "opaque" });
const [width, height] = win.getSize();
surface.width = width;
surface.height = height;
const encoder = device.createCommandEncoder();
encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0.5, b: 0.5, a: 1 },
loadOp: "clear",
storeOp: "store",
}],
}).end();
device.queue.submit([encoder.finish()]);
surface.present();
```
Build and run with: `deno desktop main.ts`, then `./main` (macOS/Linux) or `.\main.exe` (Windows).
surface.present() required to display frames
surface.present() is what actually pushes the encoded frame to the display. Without it, the window stays blank. Calling it once leaves a static frame on screen until the window closes.
Drawing a triangle with WebGPU WGSL shader
This example draws a single triangle whose vertex colors are interpolated across its face, with positions baked into the shader:
```ts
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("no WebGPU adapter available");
const device = await adapter.requestDevice();
const win = new Deno.BrowserWindow({
title: "Triangle",
width: 640,
height: 480,
});
const surface = win.getNativeWindow();
const format = navigator.gpu.getPreferredCanvasFormat();
const context = surface.getContext("webgpu");
context.configure({ device, format, alphaMode: "opaque" });
const [width, height] = win.getSize();
surface.width = width;
surface.height = height;
const shader = device.createShaderModule({
code: `
struct VertexOut {
@builtin(position) pos: vec4f,
@location(0) color: vec3f,
};
@vertex
fn vs(@builtin(vertex_index) i: u32) -> VertexOut {
var positions = array<vec2f, 3>(
vec2f( 0.0, 0.6),
vec2f(-0.6, -0.6),
vec2f( 0.6, -0.6),
);
var colors = array<vec3f, 3>(
vec3f(1.0, 0.0, 0.0),
vec3f(0.0, 1.0, 0.0),
vec3f(0.0, 0.0, 1.0),
);
var out: VertexOut;
out.pos = vec4f(positions[i], 0.0, 1.0);
out.color = colors[i];
return out;
}
@fragment
fn fs(in: VertexOut) -> @location(0) vec4f {
return vec4f(in.color, 1.0);
}
`,
});
const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: { module: shader, entryPoint: "vs" },
fragment: { module: shader, entryPoint: "fs", targets: [{ format }] },
primitive: { topology: "triangle-list" },
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0.05, g: 0.05, b: 0.08, a: 1 },
loadOp: "clear",
storeOp: "store",
}],
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
surface.present();
```
Render loop animation with setTimeout
The raw backend has no DOM, so there is no requestAnimationFrame. Schedule frames yourself using setTimeout. A self-scheduling setTimeout gives roughly one frame every 16 ms (~60 fps). Use win.isClosed() to stop the loop once the window goes away, and exit the process with Deno.exit() to prevent pending timers from keeping the runtime alive.
Animated render loop with uniform buffer example
This example reuses a triangle pipeline and passes elapsed time into the shader through a uniform buffer to spin it:
```ts
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error("no WebGPU adapter available");
const device = await adapter.requestDevice();
const win = new Deno.BrowserWindow({ title: "Spin", width: 640, height: 480 });
const surface = win.getNativeWindow();
const format = navigator.gpu.getPreferredCanvasFormat();
const context = surface.getContext("webgpu");
context.configure({ device, format, alphaMode: "opaque" });
function resize() {
const [width, height] = win.getSize();
surface.width = width;
surface.height = height;
}
resize();
win.addEventListener("resize", resize);
const shader = device.createShaderModule({
code: `
@group(0) @binding(0) var<uniform> angle: f32;
struct VertexOut {
@builtin(position) pos: vec4f,
@location(0) color: vec3f,
};
@vertex
fn vs(@builtin(vertex_index) i: u32) -> VertexOut {
var base = array<vec2f, 3>(
vec2f( 0.0, 0.6),
vec2f(-0.6, -0.6),
vec2f( 0.6, -0.6),
);
var colors = array<vec3f, 3>(
vec3f(1.0, 0.0, 0.0),
vec3f(0.0, 1.0, 0.0),
vec3f(0.0, 0.0, 1.0),
);
let s = sin(angle);
let c = cos(angle);
let p = base[i];
var out: VertexOut;
out.pos = vec4f(p.x * c - p.y * s, p.x * s + p.y * c, 0.0, 1.0);
out.color = colors[i];
return out;
}
@fragment
fn fs(in: VertexOut) -> @location(0) vec4f {
return vec4f(in.color, 1.0);
}
`,
});
const uniform = device.createBuffer({
size: 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: { module: shader, entryPoint: "vs" },
fragment: { module: shader, entryPoint: "fs", targets: [{ format }] },
primitive: { topology: "triangle-list" },
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniform } }],
});
const start = performance.now();
function frame() {
if (win.isClosed()) return;
const angle = (performance.now() - start) / 1000;
device.queue.writeBuffer(uniform, 0, new Float32Array([angle]));
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0.05, g: 0.05, b: 0.08, a: 1 },
loadOp: "clear",
storeOp: "store",
}],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
surface.present();
setTimeout(frame, 16);
}
win.addEventListener("close", () => Deno.exit(0));
frame();
```
Request adapter before wrapping window
getNativeWindow() needs an active WebGPU context and throws if called before navigator.gpu.requestAdapter(). The adapter must be requested first.
Get fresh texture each WebGPU frame
Call context.getCurrentTexture().createView() inside the render loop. The swapchain hands you a different texture per frame.
Window close() behavior with WebGPU surface
Once a surface has been taken from a window, close() hides the window instead of destroying it, so the native handles WebGPU is rendering into are not freed underneath it. Call Deno.exit() to end the process, as this is necessary to shut down the render loop.
WebGPU available in Deno 2.9+
WebGPU rendering on Deno desktop is available starting in Deno v2.9.0. For earlier versions, Deno must be updated.
BrowserWindow openDevtools method
The `openDevtools()` method opens DevTools. It accepts an optional object with boolean properties `deno` and `renderer` to control which isolates are debugged. Default behavior opens both isolates.
BrowserWindow executeJs method
The `executeJs(code)` method runs JavaScript code in the webview's main world and resolves with the result. The value crosses a realm boundary, so it must be JSON-serializable. If the script throws, the returned promise rejects with the thrown value.
Multiple windows independence
Multiple windows are independent: each has its own size, position, focus state, and webview. They can navigate to different paths or different origins, set their own bindings, and emit their own events.
BrowserWindow getNativeWindow method
The `getNativeWindow()` method wraps the window's native surface as a `Deno.UnsafeWindowSurface` so you can render to it with WebGPU. Request a GPU adapter first; the call throws if there is no active WebGPU context. Once a surface has been taken, `close()` is downgraded to `hide()` so the native handles backing the surface are not destroyed out from under WebGPU.
BrowserWindow lifecycle methods
The `Deno.BrowserWindow` class provides these lifecycle methods:
- `show()`: Show the window.
- `hide()`: Hide the window.
- `focus()`: Focus the window.
- `close()`: Send close request and fire the "close" event.
- `reload()`: Reload the webview's current document.
- `isClosed()`: Check if the window is closed.
- `isVisible()`: Check if the window is visible.
BrowserWindow windowId property
Each window has a stable numeric id accessible via the `windowId` property.
Closing a window does not stop the runtime
Closing a window does not stop the Deno runtime. The process keeps running until all windows are closed or you call `Deno.exit()`.
BrowserWindow size and position methods
The `Deno.BrowserWindow` class provides these methods for managing size and position:
- `getSize()`: Returns `[width, height]` in logical pixels.
- `setSize(width, height)`: Set the window size.
- `getPosition()`: Returns `[x, y]` position.
- `setPosition(x, y)`: Set the window position.
- `isResizable()`: Check if the window is resizable.
- `setResizable(resizable)`: Set whether the window is resizable.
- `isAlwaysOnTop()`: Check if the window is always on top.
- `setAlwaysOnTop(alwaysOnTop)`: Set whether the window stays on top.
Deno.BrowserWindow available in Deno 2.9
`deno desktop` and the `Deno.BrowserWindow` class are available starting in Deno v2.9.0.
BrowserWindow first construction adopts implicit startup window
A window opens automatically when a Deno desktop binary starts. The first `new Deno.BrowserWindow()` construction adopts that initial window. Every subsequent construction opens a new window. All windows share the same Deno runtime: there is one async runtime per process, regardless of how many windows are open.
BrowserWindowOptions constructor parameters
The `Deno.BrowserWindow` constructor accepts a `BrowserWindowOptions` object with the following fields:
| Option | Type | Default | Notes |
|--------|------|---------|-------|
| `title` | `string` | none | Window title. |
| `width` | `number` | `800` | Initial width in logical pixels. |
| `height` | `number` | `600` | Initial height in logical pixels. |
| `x`, `y` | `number` | none | Initial position; centered if omitted. |
| `resizable` | `boolean` | `true` | Whether the user can resize the window. |
| `alwaysOnTop` | `boolean` | `false` | Keep the window above others. |
| `frameless` | `boolean` | `false` | Remove the title bar and window chrome. Creation-only. |
| `noActivate` | `boolean` | `false` | Floating, non-activating panel that doesn't steal focus. Creation-only. |
| `transparentTitlebar` | `boolean` | `false` | Blend the title bar into the content. Creation-only. |
BrowserWindow creation-only options cannot be changed after construction
`frameless`, `noActivate`, and `transparentTitlebar` can only be set at creation time. `frameless` + `noActivate` together are the building blocks for tray and menu-bar popovers.
Window sizes are in logical pixels
Sizes are in logical pixels. The OS handles HiDPI scaling.
Deno does not persist window size and position between runs
Deno does not remember a window's size or position between runs. Window managers vary in whether they restore window state — on Linux/KDE, for example, each launch opens at the constructor's defaults. To persist window geometry, save it to app-owned configuration and restore it on the next startup.
BrowserWindow setTitle method
The `setTitle(title)` method changes the window title. Use a stable prefix plus a document-specific suffix, which is what users see in window switchers, the dock, and the taskbar.
BrowserWindow navigate method
The `navigate(url)` method navigates the window to a URL. Navigation works with any URL the embedded webview can load, including local HTTP server URLs (most common), `https://` URLs, `file://` URLs, and `data:` URLs. For multi-page apps, use the local HTTP server's routing rather than swapping windows. For modal dialogs, prefer creating a child window over navigating away.
BrowserWindow events
`Deno.BrowserWindow` is an `EventTarget`. Listen with `addEventListener` or assign to the matching `on<event>` property.
| Event | When it fires |
|-------|---------------|
| `resize` | The window's size changed. |
| `move` | The window's position changed. |
| `focus` | The window gained focus. |
| `blur` | The window lost focus. |
| `close` | The user requested the window close. |
| `keydown` | A key was pressed while the window was focused. |
| `keyup` | A key was released. |
| `mousemove` | The pointer moved over the window. |
| `mouseenter` | The pointer entered the window. |
| `mouseleave` | The pointer left the window. |
| `mousedown` | A mouse button was pressed. |
| `mouseup` | A mouse button was released. |
| `click` | A mouse click landed on the window. |
| `dblclick` | A double-click landed on the window. |
| `wheel` | A scroll wheel or trackpad scroll happened. |
| `menuclick` | An application-menu item was clicked. |
| `contextmenuclick` | A context-menu item was clicked. |
Pointer and keyboard events mirror their browser equivalents (`KeyboardEvent`, `MouseEvent`, `WheelEvent`). `resize`, `move`, `menuclick`, and `contextmenuclick` are `CustomEvent`s carrying a `detail` payload.
Preventing window close with preventDefault
To prevent window close (for example, to show a "Save?" dialog), listen for the `close` event and call `event.preventDefault()`. The close can then be triggered later by calling `win.close()` again.
Runtime exits when no windows are open
The runtime exits when no windows are open and there are no other live async tasks (timers, pending fetches, etc.). To exit explicitly, call `Deno.exit(0)`.