Measure, then place
Why every layout engine makes two passes over the tree, and what "definiteness" means when it travels back down.
In the code: engine/layout/measure.py
You cannot place a box until you know how big it is, and you often cannot know how big it is until you know how much room it has been given. That circularity is the central problem of layout, and essentially every engine — CSS, TeX, Qt, SwiftUI — resolves it the same way: two passes.
Pass one: bottom-up, "how big do you want to be?"
The measure pass asks every element for its intrinsic size — very nearly with no knowledge of the space available. (Two cases have to peek: a wrapping container and a definite-width column need the main-axis extent during pass one, or measure and placement would disagree about where the lines break.) Leaves answer first: a text mark measures its string, a shape adds its padding. Containers then compose their children's answers — a row sums widths and takes the max height, a column does the reverse, both adding gaps.
Nothing is positioned. The output is a single number pair per element, and on the flex path a row or column visits each child exactly once.
<diagram padding="16">
<column gap="14" align="center">
<rect>row: sum the widths, take the tallest</rect>
<row gap="10">
<rect>text</rect>
<rect>a longer label</rect>
<rect>text</rect>
</row>
</column>
</diagram>
Pass two: top-down, "here is what you actually get"
The second pass hands out real sizes. The root gets the canvas; each container
divides what it has among its children and recurses. This is where flex growth,
fr tracks, stretching, and percentages happen — all of which need to know the
available space, which only exists on the way down.
The subtle part is what CSS calls definiteness. A percentage or a stretch
only means something against a parent size that is actually known. If a column's
height comes from its content, a child asking for 50% of it is circular — so
the engine threads a flag alongside the size saying "this dimension is definite"
and treats an indefinite parent as no answer rather than as zero. Getting that
wrong is how layout engines end up with everything collapsed to nothing.
Why min-content and max-content both exist
Intrinsic sizing needs two numbers, not one, because text is squishy:
- max-content — the width if you never wrap. One long line.
- min-content — the width if you wrap at every opportunity. In practice, the widest single word.
Track sizing and flex both need these: min-content is the floor below which an item must not be squeezed, max-content the size it would take if space were free. When a single word is wider than its box even at min-content you are out of options and must break the word itself — which is why long-word splitting is a separate, slightly grubby fallback rather than part of normal wrapping.
Where the recursion bites
Grid track sizing is the awkward case. To size a track you must measure the items in it; to measure an item you may have to lay out a nested grid; and that grid's tracks need sizing. Measure and track-sizing are therefore mutually recursive, and in Sauvignon they deliberately share one module so the package's own dependency graph stays acyclic.
The costs are real: naive implementations re-measure the same subtree many times, which is why browser engines cache intrinsic sizes aggressively, and why "measure twice" can quietly become "measure 2ⁿ times" on deeply nested grids.
Further reading. The CSS Intrinsic & Extrinsic Sizing module (css-sizing-3) is the most precise description of min-content and max-content anywhere, and css-flexbox-1 §9 is a genuinely readable specification of a two-pass algorithm.