12. Flow, Grid, Stack & Tree

The router-routed layouts: they arrange boxes and text in place — or, for stack, seat them on a shared datum — then hand their links to the router (SPEC 9). flow is 1D flex, grid is 2D, stack one datum, tree a rooted hierarchy.

Flex — align / justify

layout: flow runs its children along one axis, set by direction (row horizontal — the default, and column in card content — SPEC 11). justify runs along the flow (main axis), align runs across it (cross axis). Both default center — so the knob that flushes a box’s text horizontally is justify in a row box and align in a column one, a card included.

Valuejustify (main axis)align (cross axis)
start / center / endpack at the edge / centre / oppositealign each child to the edge / centre / opposite
stretchfills children to span the main axiseach child’s box fills the cross axis
evenlyequal gaps between and around children(treated as center)
origin(treated as center)children line up origin-to-origin

stretch fills the child’s box, not its content (placed by the child’s own align/justify, also center). evenly needs multiple children.

align: origin aligns what the boxes contain, not the boxes. Every node has an origin — the bbox centre of an ordinary node, a |sketch|’s pen origin, a pattern:’s seed datum, a |drawing|’s datum (SPEC 15.1) — and origin puts every child’s origin on one shared cross-axis line, which is how a row of drawings shares one axis (SPEC 15.8). Where the line sits: given an explicit cross size the group fits into, it is the container’s centre line — a small part’s axis rides the sheet’s centreline; on an auto-sized (or overfull) axis the group centres around the line instead, so a large ensemble stays balanced. For ordinary children it is center; it differs exactly where a box is asymmetric about its origin — a view whose dimensions stack on one side. In a grid, both align and justify accept it: the cell puts the child’s origin on its track centre, so one row of cells shares a horizontal axis and one column a vertical one — the projection-sheet arrangement.

All of align/justify/stretch/evenly are no-ops unless the container is larger than its packed children — an auto-sized container has no slack to distribute (origin is the exception: it re-lines children even without slack). Slack comes from an explicit width/height, or a grid’s fixed tracks.

Grid — columns / rows / cell / span

A grid is sized by its track lists:

PropertyNotes
columnsRequired. A track list — columns: 80, 140, 80 (3 fixed), columns: repeat(3) (3 auto), or a mix (auto, 40, auto). The list length is the column count.
rowsOptional. Same form. A floor, not a cap: extra children flow into implicit auto rows. Omitted → all rows implicit, count ⌈children / columns⌉.
cellA box child’s placement column row, 1-indexed (cell: 2 1).
spanA box child’s span columns rows, default 1 1 (span: 2 = 2 1).

A track is a size (80), auto (sized to its widest/tallest child), or repeat(N) / repeat(N, size) for many equal tracks. The count comes from the list length. There is no fr unit. A fixed track is a floor like an explicit width (SPEC 5) — it grows to its widest child, so a grid never clips.

Auto-flow. Children without cell: flow left-to-right, wrapping at the column count; a cell: pins one explicitly and the rest flow around it. Bare-text cells are pure auto-flow — cell: / span: apply to box children only (a text node has no block to carry them). A grid is positional, so an empty "" cell is kept — it holds its track and keeps the cells after it aligned (in flow, an empty "" is dropped). cell: is read on a grid and on a schematic (SPEC 16.1), span: on a grid alone; where the container’s layout is statically known to be neither, they are an error (SPEC 17’s strict rule, SPEC 21: 'cell' places a grid or schematic child — this box sits in a 'layout: flow').

Per-column alignment. On a grid, align (horizontal ↔) and justify (vertical ↕) accept a list parallel to columns (one value per track) or a scalar for all — so align: start, center, end aligns three columns in one declaration. Mind the axes: a grid follows column-flow, not CSS grid, so align is horizontal — the same knob that left-aligns text in a direction: column box. stretch fills the track; start/center/end pack the cell’s box at natural size; the default centres.

A cell that fills its track (stretch) then honours its own align/ justify to place its content: an auto cell has no slack and sits centred, but a filled one slides its text to the aligned edge — what lets a |table| align a whole column (SPEC 8) with no notion of “table” in the core. The same knob aligns a multi-line text’s lines (SPEC 6).

Stack — one datum, no flow

layout: stack arranges nothing. Every child’s origin lands on the container’s datum and translate: is the only offset — not its bbox centre, so a symmetric primitive (whose origin is its centre) stacks concentric, while a |sketch|’s origin is its pen origin and several sketches keep the relationship they were drawn in. That last property is the point: flow arranges sketches side by side and throws away the frame a pen was drawn in, so co-registered geometry — artwork, a hand-placed figure, a diagram tuned past what an arranger will do — has nowhere to live without it.

{ layout: stack }
|sketch#body| { draw: move(0, 0) right(40) down(20) close() }
|sketch#slot| { draw: move(10, 5) right(20) down(10) close() }   // same frame

|stack| is |block| + layout: stack — frameless, padding 0. The container is still a box (SPEC 11): it paints, sizes to its children, and places in an enclosing flow like any other, so a stack nests inside an ordinary diagram and vice versa. Its links go to the router, so arrows, labels and routing: behave exactly as they do in a flow — absolute placement does not cost you the diagram.

gap, direction, align, justify, and gap-fill have no meaning where nothing is arranged and are ignored (SPEC 11) — the same answer a drawing gives. (A root block refuses them outright, as it does for every engine that arranges nothing.)

A stack measures in pixels. unit: and density: read here as they do on a drawing (SPEC 15.1), but the default is unit: px — one drawing unit is one pixel, 1 : 1 — because a canvas wants the number it typed. State unit: mm (and a root density:) to draw in millimetres instead.

drawing is this engine plus drafting. layout: drawing (SPEC 15) is a stack that also mates parts, lowers links to dimensions and leaders, and generates chrome; floorplan is that under an architectural vocabulary. The placement law above is stated once, here, and all three read it — which is why a |sketch|, mirror:, and pattern: work identically in every one of them, and why only the drawing half draws a fused mirror’s centreline.

Tree — rooted structure

layout: tree arranges a rooted hierarchy. Structure is |topic| nesting (SPEC 8): a direct |topic|-derived child is a branch, every other child the topic’s own content; custom structural types derive from it (|person::topic|). The scope holds exactly one root topic (none or two errors; a forest is beyond 1.0 — SPEC 24), and |topic| outside a tree scope errors (SPEC 21).

direction:GrowthThe look
column (default)down from the rootorg chart
rowrightward from the rootlogic tree / outline
bilateralboth sides, horizontallymindmap

Placement is post-order: each subtree packs its children at the cross-axis gap (sibling separation), the parent centred over its subtree’s span one main-axis gap (generation distance) away; subtrees never overlap. bilateral splits the first level — the first ⌈n/2⌉ first-level topics fill the right side top-to-bottom in declaration order, the rest the left (each half the row layout, the left mirrored, the root centred between them); a first-level side: left | right overrides its half. top / bottom there, or any side: on a row / column topic, is an error — there is no vertical bilateral; growing downward is column.

Branch links are generated, and ordinary. Desugar adds one unmarked fan per parentceo:bottom - ceo.cto:top & ceo.coo:top — written in the scope that contains the parent, with the direction’s forced sides (column: bottomtop; row: rightleft; bilateral mirrors per half, the root emitting both sides). lini desugar shows them; the scope’s routing draws them like any wire; and a link into one’s own descendant cascades as if written in that node (SPEC 4), so #cto |-| { } restyles exactly cto’s arm. An anonymous topic gets a deterministic minted id — lini-topic-N, 1-based among its scope’s topics — so its wires exist; an authored id is used as-is, and may not begin lini- (SPEC 23). Authored cross-links stay legal, never alter the tree, and keep the neutral link default. Every topic also wears a generated .lini-level-N class (root 0), so one rule restyles a tier (.lini-level-2 { font-size: 12 }).

The engine reads direction and gap alone (gap: g s — generation, then sibling; a scalar sets both; a tree scope defaults gap: 64 48, room to route at the default clearance). A plain tree is neutral — uniform topics, elbow connectors from the default routing: orthogonal; the mindmap look is the |mindmap| preset (SPEC 8).