runBrowserProgram function

Future<void> runBrowserProgram(
  1. Model model, {
  2. BrowserRunOptions options = const BrowserRunOptions(),
})

Runs a Model in the browser, rendering to an HTML5 canvas element.

This is the web/WASM program host. It:

  1. Creates or finds a canvas element
  2. Sets up a CanvasTerminalRenderer for cell rendering
  3. Creates a WebTerminalBackend with DOM event wiring
  4. Creates a WebUltravioletRenderer that bridges UV rendering to the canvas
  5. Runs the program via runProgram

Implementation

Future<void> runBrowserProgram(
  Model model, {
  BrowserRunOptions options = const BrowserRunOptions(),
}) async {
  final canvas = options.canvasId != null
      ? (web.document.getElementById(options.canvasId!)
            as web.HTMLCanvasElement)
      : web.document.createElement('canvas') as web.HTMLCanvasElement;

  if (options.appendToBody && canvas.parentNode == null) {
    web.document.body!.appendChild(canvas);
  }

  final context = canvas.getContext('2d') as web.CanvasRenderingContext2D;

  final canvasRenderer = CanvasTerminalRenderer(
    context,
    fontSize: options.fontSize,
    fontFamily: options.fontFamily,
  );
  canvasRenderer.measureFont();
  final initialSize = _viewportTerminalSize(canvasRenderer);
  _resizeCanvasToTerminal(canvas, canvasRenderer, initialSize);

  final backend = WebTerminalBackend(initialSize: initialSize);
  final terminal = BackendTerminal(backend);

  final renderer = WebUltravioletRenderer(
    terminal: terminal,
    options: const TuiRendererOptions(
      fps: 60,
      altScreen: false,
      hideCursor: true,
    ),
    canvasRenderer: canvasRenderer,
  );

  final onKeyDown = ((web.Event event) {
    final ke = event as web.KeyboardEvent;
    event.preventDefault();
    if (ke.key.length == 1) {
      backend.addInput(ke.key.codeUnits);
    } else {
      _handleSpecialKey(ke, backend);
    }
  }).toJS;

  web.document.addEventListener('keydown', onKeyDown);

  var activeMouseButton = -1;

  final onMouseDown = ((web.Event event) {
    final mouse = event as web.MouseEvent;
    event.preventDefault();
    activeMouseButton = mouse.button;
    final position = _mouseCellPosition(mouse, canvas, canvasRenderer);
    final cb = _mouseButtonCode(mouse.button) + _mouseModifiers(mouse);
    backend.addInput(
      _encodeMouseEvent(
        cb: cb,
        col: position.col,
        row: position.row,
        release: false,
      ),
    );
  }).toJS;

  final onMouseUp = ((web.Event event) {
    final mouse = event as web.MouseEvent;
    event.preventDefault();
    final position = _mouseCellPosition(mouse, canvas, canvasRenderer);
    final cb = 3 + _mouseModifiers(mouse);
    activeMouseButton = -1;
    backend.addInput(
      _encodeMouseEvent(
        cb: cb,
        col: position.col,
        row: position.row,
        release: true,
      ),
    );
  }).toJS;

  final onMouseMove = ((web.Event event) {
    final mouse = event as web.MouseEvent;
    if (activeMouseButton < 0) return;
    event.preventDefault();
    final position = _mouseCellPosition(mouse, canvas, canvasRenderer);
    final cb =
        32 + _mouseButtonCode(activeMouseButton) + _mouseModifiers(mouse);
    backend.addInput(
      _encodeMouseEvent(
        cb: cb,
        col: position.col,
        row: position.row,
        release: false,
      ),
    );
  }).toJS;

  canvas.addEventListener('mousedown', onMouseDown);
  canvas.addEventListener('mouseup', onMouseUp);
  canvas.addEventListener('mousemove', onMouseMove);

  final onWheel = ((web.Event event) {
    final wheel = event as web.WheelEvent;
    event.preventDefault();
    if (wheel.deltaY == 0) return;
    final ticks = math.max(1, (wheel.deltaY.abs() / 40).round());
    for (var i = 0; i < ticks; i++) {
      backend.addInput(_encodeWheelEvent(wheel, canvas, canvasRenderer));
    }
  }).toJS;

  canvas.addEventListener('wheel', onWheel);

  final onResize = ((web.Event _) {
    final size = _viewportTerminalSize(canvasRenderer);
    _resizeCanvasToTerminal(canvas, canvasRenderer, size);
    backend.notifySizeChanged(size);
  }).toJS;

  web.window.addEventListener('resize', onResize);

  final onBeforeUnload = ((web.Event _) {
    backend.requestShutdown();
  }).toJS;

  web.window.addEventListener('beforeunload', onBeforeUnload);

  try {
    await runProgram(model, host: null, terminal: terminal, renderer: renderer);
  } finally {
    web.document.removeEventListener('keydown', onKeyDown);
    canvas.removeEventListener('mousedown', onMouseDown);
    canvas.removeEventListener('mouseup', onMouseUp);
    canvas.removeEventListener('mousemove', onMouseMove);
    canvas.removeEventListener('wheel', onWheel);
    web.window.removeEventListener('resize', onResize);
    web.window.removeEventListener('beforeunload', onBeforeUnload);
    backend.dispose();
  }
}