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

UI

In-engine UI as ECS entities, laid out behind a single trait and rasterized through the same GPU device as everything else. This page explains why UI is retained-mode ECS with a swappable layout engine, and how it plugs into the frame. It is an explanation, not a recipe — for the steps to build a panel, see How-to: build a UI.


The contract is one trait

UI layout is the LayoutSystem trait in khora-core (ui/). It is the only thing the rest of the engine knows about layout: hand it a node tree and available space, get back computed positions and sizes. The renderer is a separate concern — a dedicated UI lane rasterizes the laid-out nodes. Splitting layout from rendering behind a trait is what lets a different layout engine drop in without touching the lanes that draw.

The default implementation is a Taffy-backed layout system in khora-infra (ui/taffy/), giving flex and grid layout. Khora maps its own UI components onto Taffy’s style model, runs the layout, and reads back per-node geometry. Taffy types never leak into components — the mapping happens inside the layout lane — so the backend stays swappable behind the trait, the same boundary discipline the rest of the engine follows.

UI is just entities

There is no separate UI tree and no separate registry. UI is entities with UI components, living in the same ECS as everything else, in the UI semantic domain. A panel is an entity with a layout node and a visual style; an image element adds an image component; hierarchy uses the same parent/child relationship as scene hierarchy — no bespoke UI parent type.

The component vocabulary is small and structural: a layout node carries sizing, min/max, padding, margin, and flex properties; a style carries background and border colour, border width and radius, and an optional texture; an image component references a texture to draw. They are screen-space by design — that is the whole reason UI uses its own layout node rather than the world-space transform the renderer uses. The concrete builder API lives in How-to: build a UI.

How it plugs into a frame

UI follows the same CLAD descent as every other domain (Control → Agent → Lane → Data), as two lanes owned by the UiAgent:

ECS (layout nodes, styles, images, hierarchy)
  ↓ UI Flow projects a UiScene into the LaneBus
layout lane   — runs the LayoutSystem, produces pre-laid-out nodes
  ↓
UI render lane — rasterizes the scene, compositing over what the renderer drew

The data layer’s UI Flow projects the UI domain into a scene View on the LaneBus; the layout lane computes geometry through the LayoutSystem trait; the render lane draws the result in the frame’s OUTPUT phase, loading the existing colour target so it composites over the scene the renderer already produced. Two passes, same shape as the scene render path: a compute-style pass followed by a draw pass.

Notably, there is no separate “UI renderer” — the UI render lane shares the one GPU device with everything else, reached through the same typed-ID abstraction. Text rendering uses a glyph cache and atlas that live in the backend because they depend on that device.

Editor UI versus game UI

Today the UiAgent runs in the editor. Its negotiation surface is minimal — one strategy, no real GORNA pressure yet — because editor UI complexity hasn’t demanded density tiers (full / simplified / hidden chrome) that the design leaves room for. In-game (play-mode) UI is on the roadmap; the path is mostly a matter of which modes the agent is allowed to run in, plus deciding the input model. The retained-mode, ECS-driven shape is the same either way.

Next steps

  • How-to: build a UI — spawn a panel and child elements with the real component API.
  • Data and the ECS — how UI components are stored, domained, and projected.
  • Rendering — the scene pass the UI overlay composites over.
  • Glossary — LayoutSystem, semantic domain, Flow, lane.