Andromeda.Window opens a real OS window on macOS, Windows, and Linux (X11 / Wayland) using winit. Windows are EventTarget-style — you attach listeners for keyboard, mouse, resize, and close events, then drive the loop with Andromeda.Window.mainloop().

The Window API ships behind the window Cargo feature. To use it you must compile Andromeda with --features window:

cargo run --features window -- run examples/window.ts

When the feature is missing, accessing Andromeda.Window or calling Andromeda.createWindow throws:

Window extension is not available. Make sure the 'window' feature is enabled.

Creating a window

const win = Andromeda.createWindow({
  title: "Hello, Andromeda",
  width: 800,
  height: 600,
});

Andromeda.createWindow(options?) is shorthand for new Andromeda.Window(options).

Options

interface CreateWindowOptions {
  title?: string; // default: "Andromeda"
  width?: number; // default: 800   (logical pixels)
  height?: number; // default: 600   (logical pixels)
  resizable?: boolean; // default: true
  visible?: boolean; // default: true
}

Instance properties

Property Type Description
rid number Internal resource id (used by canvas blit)
title string The current window title
width number Inner width in logical pixels
height number Inner height in logical pixels
closed boolean true once the window has been closed

Events

Windows expose the standard addEventListener / removeEventListener / dispatchEvent trio — attach handlers like you would on an EventTarget, and tear them down with removeEventListener.

Events fire while Andromeda.Window.mainloop() is running. The dispatched event is a plain object: { type, detail, target }.

Event detail shape
close none — the window is closing
resize ResizeEventDetail
keydown KeyEventDetail
keyup KeyEventDetail
mousemove MouseEventDetail
mousedown MouseEventDetail
mouseup MouseEventDetail

Event detail interfaces

interface ResizeEventDetail {
  width: number;
  height: number;
  scaleFactor: number;
}

interface KeyEventDetail {
  key: string; // KeyboardEvent.key (e.g. "Enter", "a")
  code: string; // KeyboardEvent.code (e.g. "Escape", "KeyA")
  keyCode: number; // legacy numeric code
  which: number; // alias of keyCode
  location: 0 | 1 | 2 | 3;
  altKey: boolean;
  ctrlKey: boolean;
  metaKey: boolean; // Command on macOS, Super on Linux
  shiftKey: boolean;
  repeat: boolean; // true for auto-repeat
  isComposing: boolean; // part of an IME composition
}

interface MouseEventDetail {
  x: number;
  y: number;
  button: number; // 0=left, 1=middle, 2=right, 3=back, 4=forward, -1 on mousemove
  buttons: number; // bitmask of currently held buttons
  altKey: boolean;
  ctrlKey: boolean;
  metaKey: boolean;
  shiftKey: boolean;
}

Example: keyboard close

const win = Andromeda.createWindow({ title: "Demo" });

win.addEventListener("keydown", (e) => {
  if (e.detail.code === "Escape") win.close();
});

win.addEventListener("close", () => {
  console.log("goodbye");
});

await Andromeda.Window.mainloop();

Rendering

present(r, g, b, a?)

Clears the swapchain to a solid RGBA color and presents one frame. Channel values are in [0, 1]. Useful as a render-loop smoke test before wiring real content.

const win = Andromeda.createWindow({ title: "Solid" });
win.present(0.2, 0.6, 0.9);
await Andromeda.Window.mainloop();

presentCanvas(canvas)

Blit an OffscreenCanvas's latest frame to the window. Andromeda shares a single wgpu device between the canvas and the window, so this is a zero-copy GPU blit — no CPU readback. The canvas can be any size; it stretches to fill the window.

presentCanvas requires the runtime to be built with both the window and canvas features. It throws otherwise.

const WIDTH = 640;
const HEIGHT = 480;

const win = Andromeda.createWindow({
  title: "Canvas",
  width: WIDTH,
  height: HEIGHT,
});
const canvas = new OffscreenCanvas(WIDTH, HEIGHT);
const ctx = canvas.getContext("2d")!;

let frame = 0;
await Andromeda.Window.mainloop(() => {
  frame++;
  const t = frame / 60;

  ctx.fillStyle = `rgb(${20 + 20 * Math.sin(t) | 0}, 40, 80)`;
  ctx.fillRect(0, 0, WIDTH, HEIGHT);

  ctx.fillStyle = "#ffcc66";
  ctx.fillRect(280, 200 + Math.sin(t) * 60, 80, 80);

  win.presentCanvas(canvas);
});

See examples/window.ts and examples/breakout.ts for full demos.

Window lifecycle

Andromeda.Window.mainloop(callback?)

Drives the winit event loop, dispatching events to every open window. Returns once every open window has been closed. The optional callback runs once per frame after events are dispatched — use it for rendering or state updates.

// Idle loop — just dispatch events.
await Andromeda.Window.mainloop();

// Per-frame callback.
await Andromeda.Window.mainloop(() => {
  // animate, render, etc.
});

close(), setTitle, setVisible, sizing

win.close(); // close and drop the window
win.setTitle("New title");
win.setVisible(false);

win.setSize(1280, 720);
const { width, height, scaleFactor } = win.getSize();

Native handle access

rawHandle() returns the OS-level window and display handles so you can hand the window off to a WebGPU surface bridge or other native interop. The shape is compatible with Deno.UnsafeWindowSurface.

interface RawWindowHandleData {
  system: "cocoa" | "win32" | "x11" | "wayland";
  windowHandle: string; // pointer-sized handle, parse with BigInt(...)
  displayHandle: string; // "0" when not applicable
  width: number;
  height: number;
}

const { system, windowHandle, displayHandle } = win.rawHandle();
console.log(system, BigInt(windowHandle), BigInt(displayHandle));

Patterns

Per-frame state machine

const win = Andromeda.createWindow({ title: "State" });
let paused = false;

win.addEventListener("keydown", (e) => {
  if (e.detail.code === "Space") paused = !paused;
});

await Andromeda.Window.mainloop(() => {
  if (paused) return;
  step();
  render();
});

Cleanly handling close

const win = Andromeda.createWindow({ title: "Cleanup" });

win.addEventListener("close", () => {
  flushPersistence();
});

await Andromeda.Window.mainloop();
Andromeda.exit(0);

Mouse tracking

let mx = 0;
let my = 0;

win.addEventListener("mousemove", (e) => {
  mx = e.detail.x;
  my = e.detail.y;
});

Limitations

  • Only one mainloop can run at a time per process.
  • presentCanvas requires the same process as the canvas — there is no cross-process surface sharing.

See Also

Found an issue with this page?Edit on GitHub
Last updated: