---
title: Desktop
description: A real library actor end to end, with signals for state, derived chains for presentation, slots for hosted apps and generator commands for motion.
---

`Desktop` animates an operating system desktop: a dock, windows that open out of their dock
icon, and a cursor. It ships in the [component library](/docs/component-library) and is large
enough to show how the patterns hold up in a real actor. The code below is trimmed from its source.
The helpers that compute dock metrics and window transforms come from an import, and it keeps
one or two of each kind of command.

```ts twoslash title="desktop.ts"
// @filename: desktop-helpers.ts
export type DesktopSkin = "aqua" | "retro";
export type DesktopDockItem = { id: string; label: string; icon?: string };
export type DesktopDockMetrics = {
  iconSize: number;
  gap: number;
  paddingX: number;
  dockWidth: number;
  left: number;
  centerY: number;
};
export type DesktopWindowRecord = {
  id: string;
  appId: string;
  title: string;
  x: number;
  y: number;
  width: number;
  height: number;
  isOpen: boolean;
  isMinimized: boolean;
  openProgress: number;
  minimizeProgress: number;
  content: { type: "hosted" } | { type: "blank" };
};
export type DesktopWindowVisual = {
  id: string;
  transform: string;
  opacity: number;
  zIndex: number;
  isFocused: boolean;
  visible: boolean;
};
export type DesktopActorWindowOptions = Pick<
  DesktopWindowRecord,
  "appId" | "title" | "x" | "y" | "width" | "height"
> & { windowId: string };
type WindowField = "x" | "y" | "width" | "height" | "openProgress" | "minimizeProgress";
export declare const clamp01: (value: number) => number;
export declare const resolveDockMetrics: (skin: DesktopSkin, itemCount: number, width: number, height: number) => DesktopDockMetrics;
export declare const buildWindowTransform: (window: DesktopWindowRecord, dockCenter: { x: number; y: number } | null) => string;
export declare const moveWindowToFront: (windows: DesktopWindowRecord[], windowId: string) => DesktopWindowRecord[];
export declare const mapWindowById: (windows: DesktopWindowRecord[], windowId: string, update: (window: DesktopWindowRecord) => DesktopWindowRecord) => DesktopWindowRecord[];
export declare const animateWindowField: (window: DesktopWindowRecord, progress: number, updates: Partial<Record<WindowField, number>>) => DesktopWindowRecord;
// @filename: desktop.ts
// ---cut---
import {
  derived,
  Easing,
  group,
  LayoutActor,
  signal,
  slot,
  type ActorClass,
  type Duration,
} from "@motionactor/core";
import {
  animateWindowField,
  buildWindowTransform,
  clamp01,
  mapWindowById,
  moveWindowToFront,
  resolveDockMetrics,
  type DesktopActorWindowOptions,
  type DesktopDockItem,
  type DesktopDockMetrics,
  type DesktopSkin,
  type DesktopWindowRecord,
  type DesktopWindowVisual,
} from "./desktop-helpers";

export class DesktopWindowHost extends LayoutActor {
  content = slot({ accepts: LayoutActor });
  window = group({ windowId: signal("") });
}

export class Desktop extends LayoutActor {
  windowHosts = slot({ accepts: DesktopWindowHost, multiple: true });

  skin = signal<DesktopSkin>("aqua");
  windows = signal<DesktopWindowRecord[]>([]);
  focusedWindowId = signal("");

  dock = group({
    items: signal<DesktopDockItem[]>([]),
    highlights: signal<Record<string, number>>({}),
  });

  mouse = group({
    x: signal(0),
    y: signal(0),
    clickProgress: signal(0),
  });

  dockMetrics = derived((): DesktopDockMetrics => {
    const { width, height } = this.localLayout();
    return resolveDockMetrics(this.skin(), this.dock.items().length, width, height);
  });

  /** appId to the screen point a window opens from and minimizes into. */
  dockIconCenters = derived(() => {
    const { iconSize, gap, paddingX, left, centerY } = this.dockMetrics();
    const centers: Record<string, { x: number; y: number }> = {};
    this.dock.items().forEach((item, index) => {
      centers[item.id] = {
        x: left + paddingX + index * (iconSize + gap) + iconSize / 2,
        y: centerY,
      };
    });
    return centers;
  });

  windowVisuals = derived((): DesktopWindowVisual[] => {
    const centers = this.dockIconCenters();
    const focused = this.focusedWindowId();
    return this.windows().map((window, index) => {
      const opacity =
        clamp01(window.openProgress) * (1 - clamp01(window.minimizeProgress));
      return {
        id: window.id,
        transform: buildWindowTransform(window, centers[window.appId] ?? null),
        opacity,
        zIndex: 100 + index,
        isFocused: focused === window.id,
        visible: (window.isOpen || window.openProgress > 0) && opacity > 0.001,
      };
    });
  });

  setDockItems(items: DesktopDockItem[]) {
    this.dock.items.set(items);
  }

  spawnActorWindow<T extends LayoutActor>(
    definition: ActorClass<T>,
    window: DesktopActorWindowOptions,
  ): T;
  spawnActorWindow(
    definition: ActorClass<LayoutActor>,
    window: DesktopActorWindowOptions,
  ): unknown {
    const host = this.windowHosts.spawn({
      window: { windowId: window.windowId },
    });
    const actor = host.content.spawn(definition);
    const { windowId, ...placement } = window;
    this.windows.set([
      ...this.windows.peek(),
      {
        ...placement,
        id: windowId,
        isOpen: false,
        isMinimized: false,
        openProgress: 0,
        minimizeProgress: 0,
        content: { type: "hosted" },
      },
    ]);
    return actor;
  }

  *moveCursor({ x, y }: { x: number; y: number }, transition: Duration = 0.6667) {
    yield* this.runtime.all(
      this.mouse.x.tween(x, { duration: transition, easing: Easing.inOutCubic }),
      this.mouse.y.tween(y, { duration: transition, easing: Easing.inOutCubic }),
    );
  }

  *openWindow(windowId: string, duration: Duration = 0.6667) {
    const baseline = moveWindowToFront(
      mapWindowById(this.windows.peek(), windowId, (window) => ({
        ...window,
        isOpen: true,
        isMinimized: false,
        minimizeProgress: 0,
      })),
      windowId,
    );
    this.windows.set(baseline);
    this.focusedWindowId.set(windowId);
    yield* this.windows.transition(
      (progress) =>
        baseline.map((window) =>
          window.id === windowId
            ? animateWindowField(window, progress, { openProgress: 1 })
            : window,
        ),
      { duration, easing: Easing.out(Easing.inCubic) },
    );
  }
}
```

## State

Everything the scene writes is a [`signal`](/docs/reactivity/signal), with related fields
namespaced by [`group`](/docs/reactivity/group). The open windows are one signal holding an
array. Opening a window changes several of its fields as one step, and the window list
changes as a whole, so one value with one writer is simpler than a nested
[`reactiveSignal`](/docs/reactivity/reactive-signal).

## Presentation

The renderers draw finished numbers. `windowVisuals` reads `dockIconCenters`, which reads
`dockMetrics`, which reads the resolved box and the dock's signals. Each is a
[`derived`](/docs/reactivity/derived), recomputed for the frame being resolved. The Aqua and
retro skins read the same `windowVisuals`, and a test can check where a window minimizes to
without drawing it.

## Hosting

Window chrome belongs to the desktop. What a window shows is any other actor. Each window
is a `DesktopWindowHost` in the desktop's `windowHosts` [slot](/docs/runtime/slots), and the app
goes into that host's `content` slot. `accepts: LayoutActor` lets any actor that can be laid
out be a window's content, so the desktop hosts apps it does not import.

`spawnActorWindow` does all three steps: spawn a host, spawn the app into it and add the
window's record to `windows`. The record starts closed, and `openWindow` animates it.

## Commands

`moveCursor` moves both axes together with `this.runtime.all`. `openWindow` writes the
structural facts first: the window is open, frontmost and focused from the first frame of
its animation. Then a [`transition`](/docs/reactivity/signal#transition) moves `openProgress`
inside the window list, so the list changes as one value each frame.

## A scene

```ts twoslash title="desktop.scene.ts"
// @filename: desktop-helpers.ts
export type DesktopSkin = "aqua" | "retro";
export type DesktopDockItem = { id: string; label: string; icon?: string };
export type DesktopDockMetrics = {
  iconSize: number;
  gap: number;
  paddingX: number;
  dockWidth: number;
  left: number;
  centerY: number;
};
export type DesktopWindowRecord = {
  id: string;
  appId: string;
  title: string;
  x: number;
  y: number;
  width: number;
  height: number;
  isOpen: boolean;
  isMinimized: boolean;
  openProgress: number;
  minimizeProgress: number;
  content: { type: "hosted" } | { type: "blank" };
};
export type DesktopWindowVisual = {
  id: string;
  transform: string;
  opacity: number;
  zIndex: number;
  isFocused: boolean;
  visible: boolean;
};
export type DesktopActorWindowOptions = Pick<
  DesktopWindowRecord,
  "appId" | "title" | "x" | "y" | "width" | "height"
> & { windowId: string };
type WindowField = "x" | "y" | "width" | "height" | "openProgress" | "minimizeProgress";
export declare const clamp01: (value: number) => number;
export declare const resolveDockMetrics: (skin: DesktopSkin, itemCount: number, width: number, height: number) => DesktopDockMetrics;
export declare const buildWindowTransform: (window: DesktopWindowRecord, dockCenter: { x: number; y: number } | null) => string;
export declare const moveWindowToFront: (windows: DesktopWindowRecord[], windowId: string) => DesktopWindowRecord[];
export declare const mapWindowById: (windows: DesktopWindowRecord[], windowId: string, update: (window: DesktopWindowRecord) => DesktopWindowRecord) => DesktopWindowRecord[];
export declare const animateWindowField: (window: DesktopWindowRecord, progress: number, updates: Partial<Record<WindowField, number>>) => DesktopWindowRecord;
// @filename: desktop.ts
import {
  derived,
  Easing,
  group,
  LayoutActor,
  signal,
  slot,
  type ActorClass,
  type Duration,
} from "@motionactor/core";
import {
  animateWindowField,
  buildWindowTransform,
  clamp01,
  mapWindowById,
  moveWindowToFront,
  resolveDockMetrics,
  type DesktopActorWindowOptions,
  type DesktopDockItem,
  type DesktopDockMetrics,
  type DesktopSkin,
  type DesktopWindowRecord,
  type DesktopWindowVisual,
} from "./desktop-helpers";

export class DesktopWindowHost extends LayoutActor {
  content = slot({ accepts: LayoutActor });
  window = group({ windowId: signal("") });
}

export class Desktop extends LayoutActor {
  windowHosts = slot({ accepts: DesktopWindowHost, multiple: true });

  skin = signal<DesktopSkin>("aqua");
  windows = signal<DesktopWindowRecord[]>([]);
  focusedWindowId = signal("");

  dock = group({
    items: signal<DesktopDockItem[]>([]),
    highlights: signal<Record<string, number>>({}),
  });

  mouse = group({
    x: signal(0),
    y: signal(0),
    clickProgress: signal(0),
  });

  dockMetrics = derived((): DesktopDockMetrics => {
    const { width, height } = this.localLayout();
    return resolveDockMetrics(this.skin(), this.dock.items().length, width, height);
  });

  /** appId to the screen point a window opens from and minimizes into. */
  dockIconCenters = derived(() => {
    const { iconSize, gap, paddingX, left, centerY } = this.dockMetrics();
    const centers: Record<string, { x: number; y: number }> = {};
    this.dock.items().forEach((item, index) => {
      centers[item.id] = {
        x: left + paddingX + index * (iconSize + gap) + iconSize / 2,
        y: centerY,
      };
    });
    return centers;
  });

  windowVisuals = derived((): DesktopWindowVisual[] => {
    const centers = this.dockIconCenters();
    const focused = this.focusedWindowId();
    return this.windows().map((window, index) => {
      const opacity =
        clamp01(window.openProgress) * (1 - clamp01(window.minimizeProgress));
      return {
        id: window.id,
        transform: buildWindowTransform(window, centers[window.appId] ?? null),
        opacity,
        zIndex: 100 + index,
        isFocused: focused === window.id,
        visible: (window.isOpen || window.openProgress > 0) && opacity > 0.001,
      };
    });
  });

  setDockItems(items: DesktopDockItem[]) {
    this.dock.items.set(items);
  }

  spawnActorWindow<T extends LayoutActor>(
    definition: ActorClass<T>,
    window: DesktopActorWindowOptions,
  ): T;
  spawnActorWindow(
    definition: ActorClass<LayoutActor>,
    window: DesktopActorWindowOptions,
  ): unknown {
    const host = this.windowHosts.spawn({
      window: { windowId: window.windowId },
    });
    const actor = host.content.spawn(definition);
    const { windowId, ...placement } = window;
    this.windows.set([
      ...this.windows.peek(),
      {
        ...placement,
        id: windowId,
        isOpen: false,
        isMinimized: false,
        openProgress: 0,
        minimizeProgress: 0,
        content: { type: "hosted" },
      },
    ]);
    return actor;
  }

  *moveCursor({ x, y }: { x: number; y: number }, transition: Duration = 0.6667) {
    yield* this.runtime.all(
      this.mouse.x.tween(x, { duration: transition, easing: Easing.inOutCubic }),
      this.mouse.y.tween(y, { duration: transition, easing: Easing.inOutCubic }),
    );
  }

  *openWindow(windowId: string, duration: Duration = 0.6667) {
    const baseline = moveWindowToFront(
      mapWindowById(this.windows.peek(), windowId, (window) => ({
        ...window,
        isOpen: true,
        isMinimized: false,
        minimizeProgress: 0,
      })),
      windowId,
    );
    this.windows.set(baseline);
    this.focusedWindowId.set(windowId);
    yield* this.windows.transition(
      (progress) =>
        baseline.map((window) =>
          window.id === windowId
            ? animateWindowField(window, progress, { openProgress: 1 })
            : window,
        ),
      { duration, easing: Easing.out(Easing.inCubic) },
    );
  }
}
// @filename: desktop.scene.ts
// ---cut---
import { createScene, LayoutActor, signal } from "@motionactor/core";
import { Desktop } from "./desktop";

class Terminal extends LayoutActor {
  output = signal("");
}

export const compiled = createScene(function* (ctx) {
  const desktop = ctx.spawn(Desktop, {
    layout: { width: 1920, height: 1080 },
  });
  desktop.setDockItems([
    { id: "browser", label: "Browser" },
    { id: "terminal", label: "Terminal" },
  ]);
  const terminal = desktop.spawnActorWindow(Terminal, {
    windowId: "terminal-window",
    appId: "terminal",
    title: "Terminal",
    x: 460,
    y: 210,
    width: 760,
    height: 420,
  });

  yield* desktop.moveCursor({ x: 960, y: 1030 }, 0.5);
  yield* desktop.openWindow("terminal-window", 0.5);
  terminal.output.set("$ pnpm test");
  yield* ctx.wait(1);
  return { desktop, terminal };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
```

The terminal is an ordinary actor with its own state, written on the same timeline as the
desktop's commands. Nothing coordinates them beyond the scene's single cursor.
