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 built on the LayoutSystem trait. Default backend is Taffy.

  • Document — Khora UI v1.0
  • Status — Authoritative
  • Date — May 2026

Contents

  1. The contract
  2. Pipeline
  3. Components
  4. The default backend — Taffy
  5. UiAgent
  6. For game developers
  7. For engine contributors
  8. Decisions
  9. Open questions

01 — The contract

UI layout is one trait in khora-core:

#![allow(unused)]
fn main() {
pub trait LayoutSystem: Send + Sync {
    fn compute(&mut self, root: NodeId, available_space: Vec2) -> Layout;
    fn add_node(&mut self, ...) -> NodeId;
    fn remove_node(&mut self, node: NodeId);
    // ...
}
}

The renderer is a separate concern — UiRenderLane rasterizes the laid-out nodes. The split lets a different layout engine (Yoga, custom) drop in by implementing LayoutSystem.

02 — Pipeline

ECS (UiTransform, UiColor, UiText, UiImage, UiBorder)
  ↓ StandardUiLane (Taffy layout computation)
UiScene (ExtractedUiNode[], ExtractedUiText[])
  ↓ UiRenderLane (rasterize to screen)

Two lanes:

  • StandardUiLane in Observe — reads UI components, runs Taffy, produces a UiScene of pre-laid-out nodes.
  • UiRenderLane in Output — rasterizes the UiScene to the swapchain. Uses LoadOp::Load so it composites over whatever the RenderAgent drew.

UiAgent owns both. Both run in the same frame.

03 — Components

ComponentPurpose
UiTransformPosition, size, anchoring (top-left, center, etc.)
UiColorBackground color
UiTextText content, font handle, color, font size
UiImageTexture handle, scale mode (stretch, tile, fit)
UiBorderBorder width and color

UI entities live in the same ECS as everything else. They use UiTransform instead of the spatial Transform because the coordinate space is screen-space, not world-space.

Hierarchy works through Parent and Children, the same as scene hierarchy. A panel with child elements is just a parent entity with UiTransform and UiColor, plus children with their own UI components.

04 — The default backend — Taffy

FilePurpose
crates/khora-infra/src/ui/taffy/mod.rsTaffyLayoutSystem — implements LayoutSystem

Taffy provides flex and grid layout. Khora maps UiTransform’s anchoring and sizing to Taffy’s Style, runs compute_layout, and reads back per-node positions and sizes.

To swap backends: implement LayoutSystem in khora-infra/src/ui/<backend>/, register it. StandardUiLane is unchanged.

05 — UiAgent

UiAgent runs in editor mode (allowed_modes: vec![EngineMode::Editor]). It exposes one strategy today — layout + render — without GORNA negotiation. As the editor’s UI complexity grows, density-based strategies (full / simplified / hidden chrome) will be added.

The agent’s execute() runs StandardUiLane in Observe and UiRenderLane in Output, both with the per-frame LaneContext.


For game developers

UI is built by spawning entities with UI components:

#![allow(unused)]
fn main() {
// A panel
let panel = world.spawn((
    UiTransform::new()
        .with_size(400.0, 300.0)
        .with_anchor(Anchor::Center),
    UiColor::rgba(0.1, 0.1, 0.12, 0.9),
    UiBorder::new(1.0, LinearRgba::WHITE),
));

// Text inside the panel
let label = world.spawn((
    UiTransform::new()
        .with_size_auto()
        .with_anchor(Anchor::TopLeft)
        .with_offset(16.0, 16.0),
    UiText::new("Hello, world.", font_handle).with_size(14.0),
));
world.add_component(label, Parent::new(panel));
}

For dynamic UI (HUD, inventory, dialog), update the components each frame in update(). The layout re-runs automatically when components change.

In-game (non-editor) UI is on the roadmap. Today, UiAgent runs only in Editor mode; the play-mode game uses external HUD code (or none).

For engine contributors

The split:

FilePurpose
crates/khora-core/src/ui/LayoutSystem trait, layout types
crates/khora-data/src/ui/UiTransform, UiColor, UiText, UiImage, UiBorder
crates/khora-lanes/src/ui_lane/StandardUiLane, UiRenderLane
crates/khora-agents/src/ui_agent/mod.rsUiAgent
crates/khora-infra/src/ui/taffy/Taffy backend

To extend the UI component vocabulary: add the component type with #[derive(Component)], register it, teach StandardUiLane to extract it, teach UiRenderLane to draw it.

To swap layout engines: implement LayoutSystem in a new backend folder, register the implementation. The lanes hold the trait object.

The text rendering uses StandardTextRenderer with a glyph cache and atlas — the cache lives in khora-infra because it depends on the GPU device.

Decisions

We said yes to

  • UI components in the same ECS. No separate UI tree, no separate registry. UI is just entities with different components.
  • LayoutSystem trait. Taffy is the default; the seam exists for replacement.
  • Two-lane split (compute + render). The compute pass is in Observe; the render pass is in Output. Same shape as the scene render path.
  • Hierarchy via Parent / Children. Same components used everywhere else. No UiParent, no UiHierarchy.

We said no to

  • An immediate-mode UI inside the engine. egui exists; we use it in some editor surfaces (currently). The engine’s first-class UI is retained-mode through ECS.
  • A separate UI rendering backend. UI rendering shares the GPU device with everything else. There is no “UI renderer.”
  • Taffy types in components. UiTransform is a Khora type. The Taffy Style mapping happens inside StandardUiLane.

Open questions

  1. In-game UI. UiAgent is currently editor-only. The path to a play-mode HUD is mostly a matter of changing allowed_modes, plus deciding the input model.
  2. Animations on UI. No tween / spring system today. Probably belongs as a separate lane that mutates UI components over time.
  3. Accessibility. Screen reader hooks, contrast modes. Not designed yet.

Next: scene save and load. See Serialization.