Laying out¶
How a height is decided¶
Every block answers one question: how tall are you at this width? Containers answer by asking their children and adding up.
Stack(a, b, c, gap=10).measure(400, theme)
# = a.measure(400) + b.measure(400) + c.measure(400) + 2 * 10
Nothing in that chain needs to know where it will end up. That comes later, and it is why adding a sentence anywhere in a figure moves everything below it without you doing anything.
Stack¶
Children top to bottom, each as tall as it asked to be.
gap defaults to theme.gap. Doc.add() already stacks what you give it, so Stack is for nesting inside something else.
Slack and grow¶
A stack normally takes exactly the height it measured. When it is given more — inside a Row that was stretched, or inside a Doc with a fixed height — the extra goes to the children marked grow=True, split evenly between them.
That is how a poster pins its footer to the bottom edge:
Doc(842, height=1191).add(
masthead,
body,
Spacer(0.0, grow=True), # absorbs whatever is left
footer,
)
And how two things get centred in a box of unknown height:
If nothing grows, the slack is simply left at the bottom.
Row¶
Children side by side, all stretched to the height of the tallest. That is what makes a row of cards line up when their text does not.
Row(left, right, gap=12.0) # even split
Row(sidebar, body, weights=(1, 3)) # a quarter, three quarters
Fixed columns¶
A child with width= takes exactly that many units; the rest divide what is left by their weights. This is the right tool for a badge, a bullet, an icon — anything whose size comes from its content rather than from the layout.
Row(
Draw(15.0, paint_badge, width=24.0), # always 24 units
Text("Networks and connectivity"), # everything else
gap=10.0,
)
Without it you end up expressing a 24-unit badge as a weight ratio, which drifts the moment the row changes width.
grid()¶
Children wrapped into rows of n. A short last row is padded with empty cells, so its blocks keep the column width of the rows above instead of spreading out:
Frame¶
A padded panel around exactly one child — the card, the band, the callout.
Frame(
child,
fill="surface", # ground, named or literal
stroke="rule", # border
stroke_width=0.8,
pad=14.0, # inherits theme.pad when None
radius=6.0, # inherits theme.radius when None
accent="red", # optional coloured spine down the left edge
height=120.0, # optional fixed height, ignoring the child
)
Frames measure their child at the width left after padding, which is why text inside a card breaks at the inner measure and not the outer one. They nest without ceremony — examples/eurostack/localisation.py puts three inside each other to draw a legal jurisdiction sitting inside a corporate one.
Vertical alignment¶
A frame's child normally starts at the top of the padded box. When the frame is taller than its child — a fixed height, or a stretched row — valign places it instead:
top (the default), middle and bottom. For blocks that are not in a frame, a Spacer(0.0, grow=True) on either side does the same job.
Image¶
A picture, embedded rather than linked, so the figure stays one self-contained file:
Give it a height; the width follows from the image's own proportions. SVG, PNG, JPEG, GIF and WebP are supported; the bytes are inlined as a data URI. align places it across the box — useful in a row, or in a column wider than the image.
An image never spills out of the box it was given: if the derived width would exceed it, the picture is scaled down to fit.
Spacer and Rule¶
Spacer(12.0) # blank vertical space
Spacer(0.0, grow=True) # a spring
Rule() # a hairline, with space around it
Rule(color="accent", stroke_width=2.0, height=2.0)
Composites¶
These return ordinary blocks; there is no new machinery behind them.
| Function | What it builds |
|---|---|
card(title, body, accent=…) |
Frame around a bold heading and a muted paragraph |
band(title, *children) |
Tinted section container with tracked-out capitals on top |
heading(text, size=…) |
Bold Text |
eyebrow(text) |
Small tracked-out capitals in the accent colour |
grid(children, columns) |
Stack of Rows, last row padded |
When one of them is almost what you want, copy its body rather than adding parameters to it — they are four lines each.
Reading a layout back¶
Any block can be named, and its rectangle is recorded when it is placed:
doc = Doc(600).add(card("A", "…", key="a"))
canvas = doc.build()
canvas.rects["a"] # Rect(x=8, y=8, w=584, h=71.3)
That register is what connectors use, and it is useful on its own when you want to check a figure from a test.