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.
| Value | justify (main axis) | align (cross axis) |
|---|---|---|
start / center / end | pack at the edge / centre / opposite | align each child to the edge / centre / opposite |
stretch | fills children to span the main axis | each child’s box fills the cross axis |
evenly | equal 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:
| Property | Notes |
|---|---|
columns | Required. 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. |
rows | Optional. Same form. A floor, not a cap: extra children flow into implicit auto rows. Omitted → all rows implicit, count ⌈children / columns⌉. |
cell | A box child’s placement column row, 1-indexed (cell: 2 1). |
span | A 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: | Growth | The look |
|---|---|---|
column (default) | down from the root | org chart |
row | rightward from the root | logic tree / outline |
bilateral | both sides, horizontally | mindmap |
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 parent — ceo:bottom - ceo.cto:top & ceo.coo:top — written in the scope
that contains the parent, with the direction’s forced sides (column:
bottom → top; row: right → left; 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).