UI
In-engine UI built on the LayoutSystem trait. Default backend is Taffy.
- Document — Khora UI v1.0
- Status — Authoritative
- Date — May 2026
Contents
- The contract
- Pipeline
- Components
- The default backend — Taffy
- UiAgent
- For game developers
- For engine contributors
- Decisions
- 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:
StandardUiLaneinObserve— reads UI components, runs Taffy, produces aUiSceneof pre-laid-out nodes.UiRenderLaneinOutput— rasterizes theUiSceneto the swapchain. UsesLoadOp::Loadso it composites over whatever theRenderAgentdrew.
UiAgent owns both. Both run in the same frame.
03 — Components
| Component | Purpose |
|---|---|
UiTransform | Position, size, anchoring (top-left, center, etc.) |
UiColor | Background color |
UiText | Text content, font handle, color, font size |
UiImage | Texture handle, scale mode (stretch, tile, fit) |
UiBorder | Border 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
| File | Purpose |
|---|---|
crates/khora-infra/src/ui/taffy/mod.rs | TaffyLayoutSystem — 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:
| File | Purpose |
|---|---|
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.rs | UiAgent |
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.
LayoutSystemtrait. Taffy is the default; the seam exists for replacement.- Two-lane split (compute + render). The compute pass is in
Observe; the render pass is inOutput. Same shape as the scene render path. - Hierarchy via
Parent/Children. Same components used everywhere else. NoUiParent, noUiHierarchy.
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.
UiTransformis a Khora type. The TaffyStylemapping happens insideStandardUiLane.
Open questions
- In-game UI.
UiAgentis currently editor-only. The path to a play-mode HUD is mostly a matter of changingallowed_modes, plus deciding the input model. - Animations on UI. No tween / spring system today. Probably belongs as a separate lane that mutates UI components over time.
- Accessibility. Screen reader hooks, contrast modes. Not designed yet.
Next: scene save and load. See Serialization.