Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Guides
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Guides8 min read

UI layout

On this page
On this pageCoordinate systemGlobal UI scaleAccessibility preferencesMotionResponsive lengths and constraintsStack layoutJustify, cross-axis alignment, and flexScrolling and clipping Back to top

Coordinates, responsive lengths, stacks, alignment, and scrolling for immediate-mode UI.

Hub: User interface · Controls: UI widgets

Stack/scroll APIs below exist on both host world.ui and game hi.ui() unless noted. Prefer hi.ui() from game modules; setUiScale / setScaleFactor / applyAccessibility are host UiContext (game scale uses hi.ui().setScale).

Coordinate system

UI positions and sizes use logical points:

  • the origin is the top-left corner;
  • positive X points right;
  • positive Y points down;
  • widget dimensions are floating-point logical values independent of display density.

UiMetrics exposes viewport_pixels, viewport_points, scale_factor, and safe_area. The engine supplies the native display scale to the world and converts physical mouse input into logical coordinates. The context converts final logical rectangles into normalized device coordinates when it emits vertices.

This keeps controls approximately the same physical size on normal and high-DPI displays. Applications may set safe-area insets through setSafeArea; root stacks and modals respect them automatically.

Global UI scale

scale_factor (the points-per-pixel divisor) is the product of two independent inputs:

  • display_scale, set via setScaleFactor — the platform's own DPI/backing scale. The engine sets this automatically at device init and on resolution/monitor changes; application code normally never touches it.
  • ui_scale, set via setUiScale (default 1) — the actual "make the UI bigger" knob, layered on top. setUiScale(1.3) renders every control, glyph, and layout dimension at 130% of its normal size, independent of whatever DPI scale the display already applies.
zig
world.ui.setUiScale(1.3);

Call it once (e.g. from an entity's onStart); it composes correctly no matter when setScaleFactor is subsequently called, so a later monitor/DPI change never clobbers the preference. This is the mechanism for an accessibility text-size setting, a "UI scale" slider, or a game simply wanting a bigger default HUD.

Accessibility preferences

ui.AccessibilityPrefs bundles ui_scale, text_scale, density, high_contrast, always_show_focus, and reduce_motion. Apply with UiContext.applyAccessibility / retained RetainedUi.applyAccessibility (theme + env + scale). High contrast uses Theme.withHighContrast() over the base palette. always_show_focus keeps focus rings visible after mouse focus (showsFocusRing()), not only after keyboard/gamepad navigation. reduce_motion snaps UI transitions (hi.motion.resolve) and freezes editor spinners.

Motion

Shared tween/spring/transition values live in hi.motion (see Motion Kit). Store drivers on entity/subsystem state; sample into styles each frame. Do not hang timelines on UiContext.

Responsive lengths and constraints

Stack, button, and modal dimensions use ui.Length:

zig
.width = .auto                    // intrinsic or parent-driven default
.width = .{ .points = 240 }       // fixed logical size
.width = .{ .percent = 0.5 }      // half the available parent dimension
.width = .fill                    // all available parent space

Combine a responsive length with minimum and maximum dimensions:

zig
var sidebar = world.ui.vStack(.{
    .width = .{ .percent = 0.30 },
    .min_width = 220,
    .max_width = 360,
});
defer sidebar.end();

Percentages and fill resolve against the parent stack's available inner size, or against the safe viewport for root stacks. Constraints are applied after resolution. This makes layouts fluid without allowing controls to become unusably small or excessively wide.

Stack layout

Widgets must be emitted inside a layout stack:

APILayout
vStackColumn (main axis down)
hStackRow (main axis right)
flowWrapping row (shared FlowCursor with retained editor layout)
zStackOverlay — children share the origin; paint order is declaration order

Stacks accept an optional stable id (engine and game SDK). Justify/flex shifts are cached by id so hit-testing matches the drawn shift — name wrappers that participate in centring rather than hand-computing positions.

zig
var column = world.ui.vStack(.{
    .id = "hud.column",
    .position = .{ 24, 24 },
    .width = .{ .points = 260 },
    .spacing = 8,
});
defer column.end();

world.ui.text("Graphics", .{});

var row = world.ui.hStack(.{ .height = .{ .points = 36 }, .spacing = 6 });
defer row.end();

_ = world.ui.button("Apply", .{ .variant = .primary });
_ = world.ui.button("Reset", .{});
zig
var chips = world.ui.flow(.{ .width = .{ .points = 200 }, .height = .{ .points = 80 }, .spacing = 6 });
defer chips.end();
for (tags) |tag| _ = world.ui.button(tag, .{ .height = .{ .points = 22 } });

var badge = world.ui.zStack(.{ .width = .{ .points = 48 }, .height = .{ .points = 48 } });
defer badge.end();
// Host: world.ui.icon(ui.icons.folder, .{ .size = 32 });
// Game: hi.ui().icon(hi.ui_types.icons.folder, 32, .primary);
world.ui.text("3", .{ .role = .accent, .size = .caption });

A stack supports these style fields:

FieldMeaning
positionOptional absolute top-left position. A root stack defaults to { 16, 16 }; a nested stack begins at its parent's cursor.
width, heightResponsive Length values. Child content receives the resolved dimensions minus padding.
min_width, min_heightLower bounds applied after responsive length resolution.
max_width, max_heightUpper bounds applied after responsive length resolution.
spacingSpace inserted after each child along the stack direction. Defaults to 8.
paddingUniform inset (all sides).
padding_x, padding_yOptional axis overrides (resolveInsets / retained chrome share the same helper).
flexWeighted free-space claim when this stack is nested in a parent (same reflow path as flexSpacer).
surfaceOptional semantic background; requires explicit width and height.

Stacks are scoped and may be nested up to 16 levels. Always close them in reverse order. defer stack.end() is recommended; calling end() explicitly before the defer is safe because it is idempotent.

Justify, cross-axis alignment, and flex

justify distributes leftover main-axis space across a stack's direct children: .start (default), .center, .end, .space_between, .space_around. Editor retained stacks use the same enum as Distribution (ui.Justify).

zig
var actions = world.ui.hStack(.{ .height = .{ .points = 36 }, .justify = .space_between });
defer actions.end();
_ = world.ui.button("Cancel", .{ .id = "dialog.cancel" });
_ = world.ui.button("Confirm", .{ .id = "dialog.confirm", .variant = .primary });

cross_align positions a child within the stack's cross axis when its resolved size is smaller than the container: .start (default), .center, .end, .stretch (forces the child to fill the cross axis regardless of its own declared size). Retained Align is an alias of this enum.

It applies to every child: controls, nested stacks, and layout leaves (text, sprites, rects, bars) alike, so a row of mixed content lines up on one centre line. Two consequences of single-pass layout:

  • .stretch is .start for anything with an explicit size. A leaf cannot be stretched, so a stretched row leaves it at the pen — which is why .stretch, the default for most rows, never moves content.
  • A content-sized child is placed when it closes, not when it opens. Its cross extent does not exist at push time, so the shift is applied at the end of the stack, once its content has measured itself. Geometry is right on the first frame; the hit rects inside it pick the shift up on the next one, exactly as justify and flex do. Declare a cross size if a block must hit-test precisely on its first frame. (flow aligns at push time only: its cross axis belongs to each wrapped line.)

flexSpacer(weight) claims a weighted share of a stack's leftover main-axis space without drawing anything. Buttons and nested stacks can also set .flex = weight so free space is distributed after them (translate-only reflow — widgets do not grow their drawn size).

zig
world.ui.text("Unsaved changes", .{ .role = .secondary });
world.ui.flexSpacer(1);
_ = world.ui.button("Save", .{ .id = "editor.save", .variant = .primary });

Geometry for justify/flex is repositioned by translating already-emitted vertices once every child's size is known (at the end of the stack). Hit-testing therefore reads the previous frame's resolved shift, which is exact once a layout has rendered one frame. Give a nested stack an explicit .id if it sits inside a justified/flexed parent and holds interactive children — but never the same id string as a control inside it: widget shifts and stack shifts share one cache (under separate keys), and reusing the string across the two is a naming collision waiting to be re-introduced. A stable tree gets correct structural ids for free.

Scrolling and clipping

A stack with .clip = true and an explicit width and height crops all descendant geometry (rectangles, borders, glyphs) to its bounds; nested clips intersect with their ancestor's. scroll_offset shifts a stack's content along its main axis before clipping, giving a scrollable region:

zig
var list = world.ui.vStack(.{
    .width = .{ .points = 240 },
    .height = .{ .points = 160 },
    .clip = true,
    .scroll_offset = scroll_position,
});
for (items) |item| world.ui.text(item.label, .{});
const max_scroll = @max(0, list.contentSize()[1] - 160);
list.end();
scroll_position = std.math.clamp(scroll_position + wheel_delta, 0, max_scroll);

contentSize() (call before end()) returns the stack's total child extent, letting callers clamp their own persisted scroll_offset each frame. For the common case, scrollArea owns the clipping, consumes native mouse-wheel input while the pointer is over it, and clamps a caller-owned offset when the area ends:

zig
var files = world.ui.scrollArea("asset-files", &file_scroll, .{
    .width = .{ .points = 240 },
    .height = .{ .points = 160 },
});
defer files.end();
for (items, 0..) |item, index| {
    var id_buffer: [64]u8 = undefined;
    const id = std.fmt.bufPrint(&id_buffer, "asset.{d}", .{index}) catch item.label;
    if (world.ui.listItem(id, item.label, selected == index, .{})) selected = index;
}

The caller-owned offset keeps scroll state explicit and stable across immediate-mode frame rebuilding. Scroll input uses a shared kinematics model (ui/scroll_physics.zig) for both immediate scrollArea and retained editor chrome:

  • Line/notch wheels (Windows mouse, macOS non-precise): wheel_y is discrete; UI multiplies by ScrollAreaStyle.wheel_step (or design-token default).
  • Precise trackpads (macOS hasPreciseScrollingDeltas): layout points × precise_gain (no × wheel_step). Live samples paint 1:1; OS momentum is flagged separately.
  • Smoothing: target/display pair; active user scroll is near-instant, coast eases. reduce_motion snaps.
  • Overscroll (default on): Apple-style asymptotic rubber (c ≈ 0.55) while the user pulls; release velocity feeds an analytic underdamped spring. OS momentum cannot pin the peak or dig further out. Scrollbar thumbs stay on-track while content bands.
  • Scrollbar thumbs paint when content overflows (scrollbar = true by default). Immediate scrollArea paints a vertical thumb; retained chrome paints the overflowing axis (or both). Thumb geometry is shared (ui.scrollbarThumb / ui.scrollbarThumbHorizontal). Retained thumbs can be dragged; immediate thumbs are display-only.
PreviousUser interfaceNext UI widgets

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/guides/ui-layout.md
On this pageCoordinate systemGlobal UI scaleAccessibility preferencesMotionResponsive lengths and constraintsStack layoutJustify, cross-axis alignment, and flexScrolling and clipping Back to top