---
title: "The editor UI"
description: "Everything you see when you edit a world — the menu bar across the top, the dockable panels, the play controls — is world-side Luau content, built from the same Z. widgets and the same engine APIs…"
section: "Topics"
slug: "topics-editor-editor-ui"
canonical: "https://origozero.ai/docs/topics-editor-editor-ui"
updated: "2026-09-03T15:00:41.900994410+00:00"
---

# The editor UI

## The shape of the editor

The editor surface is three layers, stacked top to bottom:

- **The top bar** — always present in the editor. On the left, domain menus (`Scene`, `Content`, `Render`, `Debug`, `Window`, `Settings`): click one to drop it open, hover siblings to switch between them, click away or press Esc to dismiss. In the center, the transport (below). Everything in a menu either opens a panel or runs an action.
- **The dock** — the area under the bar. Tools live here as **dockable panels**: tabs you can drag between regions, split side-by-side, stack into tab groups, and close. The editor boots with the dock *empty* so the live 3D view fills the space; tools appear only when you open them from a menu. Closing every panel returns you to a clean view of the world.
- **The world itself** — always rendering underneath. The editor is an overlay, not a window around the world. `F1` hides and shows the whole editor layer at once when you want nothing between you and the scene.

A few panels worth knowing exist out of the box: **Viewport** (the scene rendered into a dockable tab through an offscreen camera that mirrors your view), **Entities** (the scenegraph), **Execute** (a Luau console — run code against the live world), **Terminal** (the engine shell over the VFS), and the inspectors under Debug. The **Agent Chat** is a window you opt into from `Window`; it stays closed until you ask for it, and remembers your choice across mode changes.

## The transport: reading mode at a glance

The centered `▶` / `⏸` pair are **state toggles** — they always show what *is*, never what would be:

- `▶` lit means the world is playing. Press the lit button to stop and return to edit. Unlit means you're editing; press it to play.
- `⏸` lit means the gameplay clock is stopped. In edit mode it is always lit (editing *is* the world held still) and locked; in play mode it becomes interactive and pauses/resumes gameplay.

One rule governs both: lit = on, press to flip. There is no button whose label describes a future state.

## Edit mode and the editor's own liveliness

Entering edit stops the *gameplay clock* — `update` loops, physics, the clock — but the editor UI itself keeps running. If you write editor tooling (a panel, an editor-side component), this distinction reaches your code: a component that only defines `update(dt)` goes idle with gameplay, while the editor's own components also define `editorUpdate(dt)` so they stay interactive while the world is held still. Panels registered through the registry (below) don't need to care — the dock host ticks them for you.

The button above is locked lit because the editor holds the clock, not because the mode forbids running it: `engine.paused = false` in edit starts the clock back up, and then `update` and `editorUpdate` both tick, once each, every frame. An editor-side component that routes both hook bodies to one function runs it twice in that state — `core/components` has the table and the one-line guard.

## Extending the editor from your world

The built-in menus and panels register through a registry, and your world registers through the same door:

```luau
local editor = require("@builtin::modules.api.editor.registry")
local Z = require("@builtin::modules.zui")

editor.addMenu({ id = "skyworld", label = "Sky World" })

editor.addPanel({
    id = "time_of_day", label = "Time of Day", menu = "skyworld",
    build = function(state)
        return Z.vbox({
            Z.lbl("Sun angle"),
            Z.slider("tod-angle", currentAngle(), 0, 360, { onChange = "tod:angle" }),
        })
    end,
    onCallback = function(id, value, state)
        if id == "tod:angle" then setSunAngle(value); return true end
        return false
    end,
})

editor.addMenuItem({ id = "midnight", menu = "skyworld", label = "Jump to Midnight",
                     onClick = function() setSunAngle(270) end })
```

Open the editor and a **Sky World** menu sits after the built-ins; "Time of Day" opens a real dock panel — draggable, splittable, closable like Entities; "Jump to Midnight" just runs. `remove*` calls take it back out; `list()` shows what's registered. Changes apply live — the bar rebuilds from the registry, no restart.

What the editor owns so you don't have to: docking chrome and rearrangement; scheduling (your `build` runs only while the panel is open — pass `refresh` in seconds for live data, and a closed panel costs nothing); fault isolation (a `build` that errors renders as an error label instead of breaking the editor); persistence across edit ↔ play (register once, it survives mode flips); and friendly re-registration (same id replaces — hot-reloading your tooling module just works; colliding with a *built-in* id is rejected with a warning).

Put panels where a user would look: `scene` for what's in the world, `content` for assets and files, `debug` for introspection, your own menu for what's distinctly yours. An item naming a menu nobody registered lands in **Window** with a warning rather than vanishing.

## The boundary

The registry composes editor *surface* — menus, items, panels. What happens inside your panel is ordinary world code: the same widgets, callbacks, and engine APIs as anywhere else (see `man zui` for the widget vocabulary). If you need an editor *capability* that doesn't exist — a new docking behavior, a viewport interaction — that's an engine change to file an issue for, not something to simulate from a panel.
