5. Layout engine
src/layout computes every position and size that is not stored (LAY-9, ADR-5).
Esta página está em inglês; a tradução está a caminho.
src/layout computes every position and size that is not stored (LAY-9, ADR-5). The same code runs on the
host, in the script worker, in the CLI and in the webview, so a frame laid out on the canvas is laid out the
same in ud render and in use_figma reads.
5.1 What is stored and what is derived#
| Node situation | Stored | Derived |
|---|---|---|
| Child of a page, a group, a section, or a frame without auto layout | x, y, width, height, rotation, constraints |
absolute transform |
| Child in auto-layout flow | sizing per axis, min/max, grow/align overrides | x, y, and width/height on hug or fill axes |
| Absolutely positioned child of an auto-layout frame (LAY-5) | x, y, size, constraints |
absolute transform |
| Auto-layout frame | sizing per axis, fixed sizes where fixed | size on hug or fill axes |
| Group, boolean operation | children | bounds (Figma: a group’s box is its children’s union) |
| Text | sizing mode (auto width, auto height, fixed), fixed sizes where fixed | size on auto axes (§5.6) |
Constraints (LAY-1) are applied when a parent’s size changes through a command: node.resize on a frame runs
the constraint solver and writes the children’s new stored x/y/width/height as events (unless Ctrl is
held, which the gesture passes as ignoreConstraints). Constraints are therefore a command-time computation;
auto layout is a read-time derivation.
5.2 Auto layout: flex#
For a frame with layoutMode HORIZONTAL or VERTICAL (optionally layoutWrap: "WRAP"), the engine follows
Figma’s model, which is close to CSS flexbox with box-sizing: border-box:
- Inner box: frame size minus padding (and minus strokes when “include stroke in layout” is on, LAY-6).
- Measure each flow child on the main axis: fixed → its size; hug → its content size (recursively); fill → 0 for now. Text children measure through the text engine (§5.6).
- Lines: without wrap, one line. With wrap, break into lines when the next item plus
itemSpacingexceeds the inner main size;counterAxisSpacingseparates lines (LAY-2). - Distribute per line: free space = inner main size − sum of measured sizes − gaps. Fill children share it
equally (Figma has no flex weights). Min/max: a fill child whose share violates its min or max is frozen at
the limit and the rest is redistributed, repeating until stable (the CSS “resolve flexible lengths” loop;
at most
nrounds fornfill children). - Gap: a number, or
"auto"(space between): the free space is split between items and fill children are not allowed (the command refuses fill with auto gap, as Figma does). - Counter axis: fixed → its size; hug → content; fill → line’s cross size (the inner cross size without
wrap). Alignment: the 9-point
primaryAxisAlignItems×counterAxisAlignItems, plusBASELINEfor horizontal layouts, which aligns first baselines taken from the text engine (LAY-2). - Reverse z-order (
itemReverseZIndex) changes paint order only, not positions. - Hug parent: the frame’s own hug size is the content extent plus padding, clamped by its min/max.
Hug and fill on the same axis between parent and child is resolved as Figma does: setting a child to fill on an axis where its parent hugs changes the parent to fixed on that axis, as an extra event of the same command.
5.3 Auto layout: grid#
layoutMode: "GRID" (LAY-4) places flow children in tracks:
- Tracks are fixed (px), fill (
fr) or hug (largest content of the items in the track). - Explicit placement by row and column with spans; other children are auto-placed row by row into the first free cell, in sibling order.
- Track sizing: fixed first, then hug from single-span items, then multi-span items grow hug tracks evenly, then
frtracks share what is left (CSS grid’s algorithm restricted to these three track kinds). - Items align within their cell by the same 9-point alignment, with fill meaning “stretch to the cell”.
5.4 Sizing, min and max#
Every node in flow has layoutSizingHorizontal and layoutSizingVertical in FIXED | HUG | FILL (LAY-3), the
Plugin API names. minWidth, maxWidth, minHeight, maxHeight clamp the result of every sizing mode,
including hug. A hug size is computed with the child’s own min/max applied first, then the parent’s
distribution, then the clamp, in that order on every axis, so the result does not depend on traversal order.
5.5 Absolute children#
A child with layoutPositioning: "ABSOLUTE" is skipped by flow and laid out against the frame’s box by its
constraints after the frame’s size is known. It does not contribute to a hug size (Figma’s rule).
5.6 Text auto-resize#
Text and layout depend on each other: a text’s height depends on its width, and its width may depend on the layout. The engine asks the text engine for measurements with an explicit available width:
textAutoResize |
In flow, main axis = width | Height |
|---|---|---|
WIDTH_AND_HEIGHT (auto width) |
hug: the widest line with no wrapping (max-content) | sum of line heights |
HEIGHT (auto height) |
width from fixed or fill; hug behaves as fixed at the stored width | lines at that width |
NONE / TRUNCATE (fixed) |
stored | stored; overflow clipped or ellipsised (TXT-1) |
A fill text in a horizontal hug row would be circular; it is measured at max-content, as Figma does. Text measurements are cached by (text node, resolved style hash, available width rounded to 1/64 px).
5.7 Incremental relayout#
A fold or a draft mutation marks nodes dirty. Dirtiness propagates:
- up while the parent’s size depends on its children (hug on any axis, groups, booleans);
- down to flow children when a frame’s inner size, padding, gap, alignment or direction changed, and to fill children of any frame whose size changed;
- to every instance of a component whose layout changed (through
instancesOf).
Relayout then runs from the highest dirty root, top-down, reusing cached measurements of clean subtrees. Each
frame’s result is cached with the input key (inner width, inner height, style hash), so a parent that
re-measures a clean child at the same width gets the cached answer. Dragging a node inside an auto-layout frame
re-lays out that frame and its hug ancestors only.
5.8 Determinism#
- Children are visited in fractional-index order, never in map order.
- Only
+ − × ÷,min,max,absandsqrtare used in layout; these are exactly specified by IEEE 754. Rotation usescore/math.ts’s ownsin/cos(a port of fdlibm), becauseMath.sinis not required to give the same bits in every engine version. - Floating-point results are not rounded inside the engine; the pixel snapping of the renderer is separate.
test/layout/runs every fixture in Node and, through the dev page, in Chromium, and compares the derived geometry bit for bit.