Dock, stack, anchors, margins, padding, and safe areas.
Layout
Layout decides where every HUD node sits and how big it is. DemiEngine measures everything in canvas units from a design resolution you choose, then scales to the real screen. You describe intent — “bar on top”, “column with gaps”, “corner badge” — and the runtime resolves positions. This page explains each placement tool and when to reach for it.
Canvas units
A HUD document declares a design size:
{
"format_version": 1,
"canvas_size": [1280, 720],
"children": []
}
canvas_sizeis[width, height]in design units. Every position, size, gap, and margin is measured against it.- A 1280×720 canvas on a 2560×1440 screen renders at 2× — a 140-unit button becomes 280 physical pixels.
- Aspect ratio is preserved with letterboxing or safe-area padding rather than stretching, so layout does not shear on phones.
Why units instead of pixels: one authoring pass works for desktop, tablet, and phone. A 24-unit gap is the same visual proportion everywhere.
dock
dock pins a node to an edge or fills its parent. Values: fill, top, bottom, left, right, center.
fill
{
"id": "hud_root",
"type": "container",
"dock": "fill",
"children": []
}
Stretches the node to cover its parent completely. Use it for backgrounds and full-screen stacks.
top bar
{
"id": "top_bar",
"type": "panel",
"dock": "top",
"size": [1280, 64],
"background_color": "#10121ccc",
"children": []
}
Anchors the panel to the top edge of its parent. Its height comes from size; its width stretches across the parent, minus margins. Docked siblings do not reserve space for one another, so place a body below this bar with padding or an explicit position.
side panel
{
"id": "score_panel",
"type": "panel",
"dock": "right",
"size": [320, 720],
"margin": [0, 16, 16, 16],
"stack": "column",
"pad": 16,
"gap": 8,
"children": []
}
dock: "right" anchors the panel's left edge to the parent's right edge. The negative left margin moves its 320-unit width back inside. bottom likewise needs a negative top offset for a fixed-height node. dock: "center" anchors the node's top-left at the parent's center; offset by half its size to center the rectangle.
Each dock shorthand sets anchor points for that node. A fill node spans its parent and can overlap docked siblings; use a layout container or explicit offsets where nodes must not overlap.
Right and bottom docking align the control's outer edge inside its parent; positive right/bottom margins inset it. Center docking centers the whole control in the margin-adjusted parent. Alignment uses the final size, including min/max constraints, and follows resizing. position or at adds an optional offset. Explicit anchors override docking and retain top-left-origin semantics.
Development migration: remove negative offsets previously used only to compensate for right, bottom or center docking. The editor now writes the dock value without baking a size-dependent position. Undocking preserves the visible position.
stack
stack lays children in a run. Values: row, column, grid.
row and column
[
{
"id": "toolbar",
"type": "container",
"stack": "row",
"gap": 8,
"alignment": "center",
"children": []
},
{
"id": "menu",
"type": "container",
"stack": "column",
"gap": 12,
"alignment": "stretch",
"children": []
}
]
stack— direction children flow.rowis left to right,columnis top to bottom.gap— empty canvas units between children.alignment— how children sit across the flow:start,center,end, orstretch(children fill the cross axis).
grid
{
"id": "ability_grid",
"type": "container",
"stack": "grid",
"columns": 4,
"gap": 6,
"children": []
}
columns— how many cells per row before wrapping. Required forgrid.gapapplies between both rows and columns.
To fill each grid cell horizontally, give its child anchor_min: [0, 0], anchor_max: [1, 0] and a fixed height. A column additionally needs alignment: "stretch". A zero width alone does not mean stretch.
The grid fills cells left to right, top to bottom. Skyforge’s skill bar is a 4-column grid of 64-unit icons.
anchors and margin for corners
For a badge that hugs a corner at a fixed size, anchor a point of the node to a point of its parent and offset with margin.
{
"id": "fps_badge",
"type": "label",
"text": "60 FPS",
"anchor_min": [1, 1],
"anchor_max": [1, 1],
"margin": [-140, -48, 0, 0],
"size": [124, 32]
}
anchor_min/anchor_max— each is[x, y]in 0…1 of the parent’s box.[0, 0]is the top-left,[1, 1]is the bottom-right. When min equals max, the node anchors to one point; when they differ, the node stretches between the two points.margin—[left, top, right, bottom]in canvas units. For a fixed-size node anchored at the bottom-right, negative left/top values move it inward; right/bottom matter when the anchors stretch.size— width and height in canvas units when the node is not stretched.
Corner recipes: top-left anchor_min/max [0, 0] with positive left/top margins; top-right [1, 0] with a negative left offset; bottom-left [0, 1] with a negative top offset; bottom-right [1, 1] with negative left/top offsets. Prefer dock when a node spans an edge.
pad, padding, margin
Three similar words, three different boxes:
| Field | Applies to | Meaning |
|---|---|---|
pad | a container | Shorthand: one number for equal inner space on all four sides. Keeps children away from the container’s edge. |
padding | a container | Full form inner space: a number, or [left, top, right, bottom]. Same effect as pad with per-side control. |
margin | any node | Outer space between this node and its parent or siblings (with anchors it offsets from the anchor point). |
{
"id": "card",
"type": "panel",
"padding": [16, 24, 16, 24],
"margin": 12,
"children": []
}
This card sits 12 units away from its neighbours and keeps 16/24 units of air inside around its children.
min_size and max_size
{
"id": "tooltip",
"type": "panel",
"min_size": [160, 48],
"max_size": [360, 240],
"children": []
}
min_size—[width, height]the node never shrinks below. Stops tiny text from collapsing.max_size—[width, height]the node never grows beyond. Stops a long label from crossing the screen.
The layout clamps the authored or flow-computed size into the min/max range; it does not measure text to choose an intrinsic size.
at for absolute placement
at places a node at an explicit canvas point instead of flowing it. Use it for overlays that must not shift with siblings.
{
"id": "crosshair",
"type": "image",
"texture": "asset://ui/crosshair",
"at": [624, 344],
"size": [32, 32]
}
at—[x, y]of the node’s placement point in canvas units, measured from the parent’s top-left.size— the node’s extent in canvas units.
The crosshair's top-left is at [624, 344], placing its 32×32 rectangle at the center of a 1280×720 canvas. Prefer dock and stack for anything that shares space with siblings.
Safe areas
Phone notches, rounded corners, and home bars cover parts of the screen. A safe area is the region free of those obstructions. DemiEngine respects it by default so text and buttons stay reachable.
{
"id": "sky_bg",
"type": "image",
"texture": "asset://ui/sky",
"dock": "fill",
"respect_safe_area": false,
"children": []
}
respect_safe_area— boolean, defaulttrue. The node lays out inside the safe rectangle.- Set
respect_safe_area: falseonly for intentional edge-to-edge work: full-screen backgrounds, letterbox fills, decorative art that may run under a notch. - Keep readable text and interactive controls inside the safe area even on an edge-to-edge parent — a button under the home bar cannot be tapped.
Skyforge’s practice: the sky gradient image is respect_safe_area: false; the health bar, buttons, and labels stay on their safe defaults.
Next
- HUD nodes — every node type and its fields.
- Text, theme & locale — fonts, wrapping, styles.
- UI & HUD — the overview page.
Control rendering and input order
corner_radius rounds native panel/control fills and borders. Text inputs apply their padding to placeholder text, entered text, selection, composition and caret, and honor vertical text alignment. Sliders render a narrow track with a thumb; progress controls fill their authored bounds.
Pointer hit testing follows the same effective layer order as rendering, including inherited visibility and scroll clipping. focusable: false removes a control from keyboard focus traversal while retaining pointer activation. This is useful for a transparent outside-click target behind a popup; set both its background and hover colors transparent.