v0.7.1 (build 925)

What this page covers #

Koya layout is flow- and grid-oriented (not CSS). This page explains exactly how slots are computed, how children are measured, and which properties own placement versus visual transforms.

By task #

Overview #

Containers position direct children using row, column, or grid rules. Children contribute margins and optional sizes. Visual transforms (position/scale/rotation/animation) apply after layout and do not change the layout calculation.

Import #

import * as Compositor from 'Koya/Compositor';
import * as UI from 'Helix/UserInterface';

Mental model #

  • A parent with layout positions its direct children into slots.
  • Each child can optionally specify a layout size (item.size) and a margin. If no size is given, the parent measures the child's renderable bounds.
  • After a slot is assigned, the child's renderable can be aligned inside that slot with contentAlign. This is separate from the parent's alignment options.
  • Text has its own glyph alignment (renderable.justify) which is separate again.
  • Animations and renderable transforms are visual only and do not change slot sizes or flow decisions.

A quick map of "who controls what":

  • Parent container: layout.justifyContent (main axis), layout.alignItems (cross axis)
  • Child override: item.alignSelf (cross axis only)
  • Inside the child's slot: contentAlign on the child element
  • Inside text geometry: renderable.justify for text

Container types #

  • row: left-to-right flow with optional wrapping into new rows.
  • column: top-to-bottom flow with optional wrapping into new columns.
  • grid: a simple grid by rows or columns, with spans and per-axis alignment.
  • none: not a flow/grid container. Children inherit the container's inner bounds (see Layout type: none).

Container properties #

Set on layout when creating the container (or later via UI.setLayout).

layout
Container layout configuration.
  • type 'row' | 'column' | 'grid' | 'none' - Layout mode for positioning children.
  • wrap boolean optional - Row: wrap to next row; column: wrap to next column.
  • wrapLimit number optional - Max items per line/column when wrapping.
  • gap number optional - Uniform spacing between items along the main axis; also used as row/column gap fallback in grid.
  • padding { t, r, b, l } optional - Inner inset.
  • justifyContent 'start' | 'center' | 'end' | 'space-between' optional - Distribution along main axis.
  • alignItems 'start' | 'center' | 'end' optional - Alignment along cross axis.
  • gridColumns number optional - Grid only. If zero, inferred from average child width.
  • gridRows number optional - Grid only. If zero, inferred to fit all items.
  • columnGap number optional - Grid only. Override gap for column spacing.
  • rowGap number optional - Grid only. Override gap for row spacing.
  • gridFlowByRow boolean optional - Grid only. true = fill by rows, false = by columns.
  • gridAlignItems 'start' | 'center' | 'end' optional - Grid only. Horizontal alignment inside each grid cell.
  • gridJustifyItems 'start' | 'center' | 'end' | 'space-between' optional - Grid only. Vertical distribution inside each grid cell.
Note

The container's available area is its bounds minus padding. Root elements get a canvas-sized bounds before layout.

Cross-axis alignment uses the container's computed size and children's metrics; if you want a larger slot than the content, provide item.size on the child.

Child (item) properties #

Set per child under item when creating the child (or later via setters).

item
Per-child layout properties.
  • margin { t?, r?, b?, l? } optional - Child margin.
  • size { x?: number | 'auto', y?: number | 'auto' } optional - Omit or pass 'auto' to use the parent's measurement of the child's renderable bounds for that axis.
  • position { x, y } optional - Absolute placement when parent layout.type === 'none'. Places top-left of the child slot at this offset.
  • aabb { min: {x,y}, max: {x,y} } optional - Absolute rectangle inside the parent's inner area; overrides position and size. Only when parent is 'none'.
  • order number optional - Lower orders come first; ties are stable.
  • flexGrow number optional - Share of remaining main-axis space. Only applies when there is positive leftover space.
  • alignSelf 'start' | 'center' | 'end' optional - Cross-axis override for this child.
  • gridColumn number optional - Grid only. 1-based column placement.
  • gridRow number optional - Grid only. 1-based row placement.
  • gridColumnSpan number optional - Grid only. Number of columns to span.
  • gridRowSpan number optional - Grid only. Number of rows to span.
Note

Size special cases in flow: - Cross-axis: when the measured cross size is zero, the child fills the container's inner cross size (minus margins). Row: height=0 → height becomes inner height. Column: width=0 → width becomes inner width. - Main-axis: 'auto' (or omitted) uses the measured size as the base; if flexGrow > 0, it participates in distributing remaining space. - You can also set size later via UI.setLayoutSize(win, id, { x?, y? }). - flexShrink exists in the API but is not applied in the current layout algorithm.

Sizing and measurement rules #

  • If item.size is provided (> 0), that value is used for the child's slot size along that axis.
  • If not provided or auto, the parent measures the child's renderable local bounds for that axis.
  • Flow cross-axis auto-fill: when the measured cross size is zero, the child fills the container's inner cross size (minus margins).
  • Flow main-axis growth: remaining main-axis space is shared only among children with flexGrow > 0.
  • minWidth/minHeight/maxWidth/maxHeight are applied to the measured/preferred size if provided via item.
  • Margins reduce the usable slot area and affect positioning.

Flow layout (row/column) — how it actually works #

  1. Inner area = container.bounds minus padding.
  2. Build an ordered child list: sort by item.order (stable by id for ties).
  3. For each child, determine its base size: use item.size (w/h > 0) if provided; otherwise measure renderable bounds. Apply min/max constraints if present.
  4. Break into lines (rows for row, columns for column): start a new line if wrap is true and the next item would overflow the main limit, or wrapLimit would be exceeded.
  5. For each line, compute layout: remaining space = max(0, mainLimit - line.mainSize). Apply justifyContent (start, center, end, or space-between). If remaining > 0 and total grow > 0, distribute via flexGrow: each item gets share = (item.grow / totalGrow) * remaining.
  6. Place each item, honoring per-item margins and alignSelf override for cross-axis alignment. The cross-axis uses alignItems if alignSelf is not set.
  7. Set each child's bounds to the resolved slot rectangle (this is the area contentAlign can use inside the child).
  8. Advance to the next line using the line's cross-size plus gap.

What this means in practice:

  • flexGrow only distributes positive leftover space. It does not currently shrink items when overflowed.
  • space-between distributes leftover space between items after growth is considered.
  • To visually centre a small child in a larger region, either give the child a larger slot via item.size and set contentAlign: { x: 'center', y: 'center' }, or for text set renderable.justify: 'center'.

Grid layout — how it actually works #

  1. Inner area = container.bounds minus padding.
  2. Determine columns/rows: if gridColumns <= 0, infer a column count using the average preferred width of children. If gridRows <= 0, infer rows to fit all items.
  3. Compute cellW and cellH from inner area minus gaps, divided by cols/rows (clamped to non-negative).
  4. For each child (in order): choose explicit cell (gridColumn/gridRow if provided, 1-based) or auto-place using an advancing cursor (by rows if gridFlowByRow). Apply gridColumnSpan/gridRowSpan to compute the slot rectangle.
  5. Align inside the cell: horizontal via gridAlignItems, vertical via gridJustifyItems. Margins are applied inside the resulting slot.
  6. Set each child's bounds accordingly, then recurse.

Layout type: none #

If layout.type === 'none' (or omitted): the container does not perform flow/grid positioning. Instead, each child's bounds is set to the container's inner area (minus the child's margins). This keeps the child visually "full-bleed" beneath the container unless you:

- Move the child's renderable with transforms, or - Use contentAlign to position the renderable within that full-area slot, or - Give the child its own layout (making it a nested container).

This is intentionally different from CSS absolute positioning. Think of none as "no flow; children inherit the available rectangle."

Content alignment inside a slot #

On each child element:

  • contentAlign: { x?: 'start'|'center'|'end', y?: 'start'|'center'|'end' } | 'fill' — positions the renderable inside the child's slot (bounds).
  • contentPositioning: 'raw'|'contain' — controls whether content is clamped inside the slot (contain) or allowed to overflow (raw).
Note

'fill' mode: makes the renderable fill the element's OUTER bounds. Padding still applies to children, but is ignored for the renderable. Only renderables that implement inherit-bounds will resize geometry (currently box). Others ignore 'fill'. contentPositioning and {x,y} alignment are ignored when 'fill' is used.

Scripting API #

UI.setLayout
Update container layout (type, wrap, wrapLimit, gap, padding, justifyContent, alignItems, and grid options).
UI.setLayout(windowId, id, props)
UI.setMargin
Set a child's margin.
UI.setMargin(windowId, id, { t, r, b, l })
UI.setLayoutSize
Set a child's width/height (partials allowed). 'auto' switches the axis back to measured/auto semantics.
UI.setLayoutSize(windowId, id, { x?, y? })
UI.setLayoutOrder
Set draw/layout order.
UI.setLayoutOrder(windowId, id, order)
UI.setGrow
Set flexGrow.
UI.setGrow(windowId, id, number)
UI.setContentAlign
Set content alignment inside the slot.
UI.setContentAlign(windowId, id, { x?, y? })
UI.setContentPositioning
Set whether content is clamped or allowed to overflow.
UI.setContentPositioning(windowId, id, 'raw' | 'contain')

All mutators mark layout as dirty; layout is recomputed before each render.

Recipes #

Centre text inside a fixed-size slot:

{
  renderable: { type: 'text', string: 'Hello', size: 20, font: '/rom/fonts/SourceSans3-Regular.ttf' },
  item: { size: { x: 200, y: 80 } },
  contentAlign: { x: 'center', y: 'center' }
}

Right-align items in a row, with even spacing between them:

{
  layout: {
    type: 'row', gap: 8,
    justifyContent: 'space-between',
    alignItems: 'center'
  }
}

Grow a middle spacer between two fixed items:

child: [
  { renderable: { type: 'text', string: 'Left', size: 18, font: '/rom/fonts/SourceSans3-Regular.ttf' } },
  { renderable: { type: 'panel' }, item: { flexGrow: 1 } },
  { renderable: { type: 'text', string: 'Right', size: 18, font: '/rom/fonts/SourceSans3-Regular.ttf' } }
]

Grid with two columns and centred items:

{
  layout: {
    type: 'grid', gridColumns: 2,
    columnGap: 8, rowGap: 6,
    gridAlignItems: 'center',
    gridJustifyItems: 'center'
  }
}

Working example #

Creates an overlay window with a padded row container that spaces three children: two text labels pinned to each end and a flexible spacer in the middle. This is a common toolbar/status-bar pattern.

import * as Compositor from 'Koya/Compositor';
import * as UI from 'Helix/UserInterface';

const win = await Compositor.createWindow({
  role: 'overlay',
  anchor: 'top-left',
  size: { x: 360, y: 200 },
  offset: { x: 100, y: 100 }
});

const bar = await UI.createElement(win, {
  renderable: { type: 'panel' },
  layout: {
    type: 'row', gap: 8,
    padding: { t: 6, r: 10, b: 6, l: 10 },
    justifyContent: 'space-between',
    alignItems: 'center'
  },
  child: [
    { renderable: { type: 'text', string: 'Left', size: 18, font: '/rom/fonts/SourceSans3-Regular.ttf' } },
    { renderable: { type: 'panel' }, item: { flexGrow: 1 } },
    { renderable: { type: 'text', string: 'Right', size: 18, font: '/rom/fonts/SourceSans3-Regular.ttf' } }
  ]
});
await UI.attachRoot(win, bar);

Behaviour notes and gotchas #

  • Layout is independent from visual transforms and animations.
  • No margin collapsing.
  • flexGrow distributes only positive leftover space. It does not currently shrink items when overflowed.
  • space-between distributes leftover space after computing base line size; items with flexGrow can consume some/all of that space first.
  • Order is stable: equal order values keep source order.

Final tips #

  • If the result is not what you expect, inspect which layer you intend to control: container alignment, child override, slot alignment, or text glyph alignment.
  • Prefer explicit item.size when you want reliable centring or spacing independent of content measurement.
  • For text, always set font (e.g. /rom/fonts/SourceSans3-Regular.ttf) and a size for predictable results.

For overflow content, enable native scrolling with scroll: true or axis/options on the viewport. Native scrolling clips children and translates their layout bounds, so hit testing follows their visible positions. Scroll limits use immediate child bounds, margins, and padding; give a wrapper an explicit overflow size. See Native scrolling for raw APIs or ScrollView for the standard viewport/content control.