Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Run in a browser

Every backend compiles to wasm32-unknown-unknown. Which one to use depends on what the game looks like in the browser: a text UI inside a terminal emulator widget, or a canvas.

Text UI: terminal-wasm

retroglyph-terminal-wasm implements Backend directly, like Headless: there’s no event loop in this crate at all. A browser terminal emulator (xterm.js, or any other; the crate has no dependency on one) is driven from JS, which calls in once per animation frame to pull freshly rendered ANSI bytes and push back whatever input it collected. On wasm32 the crate exposes free functions (wasm_terminal_new, wasm_terminal_resize, wasm_terminal_push_key, wasm_terminal_take_output, plus mouse/paste/focus variants) that drive a TerminalWasm by opaque handle. Here’s the crate’s own reference driver for xterm.js in full:

import init, {
  wasm_terminal_new,
  wasm_terminal_resize,
  wasm_terminal_push_key,
  wasm_terminal_take_output,
} from './pkg.js';

// `code` values above 0x110000 select a named key; see this crate's `key_codes` module for the
// full list (arrows, Home/End, F1-F24, etc).
const NAMED_KEY_BASE = 0x00110000;
const KEY_ENTER = NAMED_KEY_BASE + 1;
const KEY_BACKSPACE = NAMED_KEY_BASE;

// `mods` is a bitmask: SHIFT = 1, CONTROL = 2, ALT = 4, SUPER = 8.
function decodeXtermData(data) {
  if (data === '\r') return { code: KEY_ENTER, mods: 0 };
  if (data === '\x7f') return { code: KEY_BACKSPACE, mods: 0 };
  // A single printable character forwards as its Unicode codepoint; xterm.js already resolves
  // Shift into the codepoint itself (e.g. 'A' vs 'a'), so no SHIFT bit is needed here.
  if (data.length === 1) return { code: data.codePointAt(0), mods: 0 };
  return null;
}

async function main() {
  await init();

  const term = new Terminal({ cols: 80, rows: 24 });
  term.open(document.getElementById('screen'));

  const handle = wasm_terminal_new(term.cols, term.rows);

  term.onData((data) => {
    const key = decodeXtermData(data);
    if (key) wasm_terminal_push_key(handle, key.code, key.mods);
  });

  window.addEventListener('resize', () => {
    // Call whatever fit-to-container logic resizes `term` first (e.g. xterm.js's FitAddon), then
    // tell the backend to match.
    wasm_terminal_resize(handle, term.cols, term.rows);
  });

  function frame() {
    const ansi = wasm_terminal_take_output(handle);
    if (ansi) term.write(ansi);
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
}

main();

That’s a wiring template, not a full game: it plumbs input/output through the FFI but calls no per-frame drawing logic of its own; that’s still your Rust code, holding a Terminal<TerminalWasm> the same way it would hold a Terminal<Headless> in a test.

A game built on retroglyph-core’s App trait usually wants this crate’s app_entry! macro instead of driving TerminalWasm by hand: it generates a single-instance-per-page FFI surface that owns the Terminal<TerminalWasm> and drives App::update for you, including a backgrounded-tab delta clamp. See docs.rs for both.

Canvas: software, gl, or wgpu

All three windowed backends port to wasm32 unchanged via winit’s web backend: the same run_windowed/run_app call that opens a native window targets a <canvas> element in the browser instead, with gl speaking WebGL2 and wgpu speaking WebGPU on that target. See Choose a backend for which of the three to reach for. One behavioral difference to know about porting from native: on wasm32 the browser owns frame pacing (winit services each requested redraw on the next requestAnimationFrame), so WindowConfig::fit’s target_fps cap is a native-only optimization; an app that relies on it to throttle below the display refresh rate will run uncapped once it’s running in a browser tab.

Building and packaging

Add the wasm32-unknown-unknown target and wasm-bindgen, then build your binary/example the same way you would natively, aimed at that target:

rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli
cargo build --target wasm32-unknown-unknown --release --features software  # or gl, wgpu, terminal-wasm
wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/your_game.wasm

wasm-bindgen’s output is an ES module (pkg/your_game.js) plus the .wasm binary; serve them over real HTTP (fetch()-ing a .wasm module is blocked from a file:// origin) alongside an HTML page that calls the generated init() before anything else. tools/build-wasm-example.sh in this workspace is a complete, working reference for exactly this build-and-package step, used to produce the live examples gallery: every example in this repo runs there in all four wasm-capable variants (headless text, terminal-wasm, software canvas, gl WebGL2) with no local toolchain required to try one.

See also

  • Choose a backend for the terminal-vs-canvas decision in full.
  • Handle resize: a browser window/canvas resizes the same way a native one does, through Event::Resize.