Skip to content

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.

Stack(heading, body, footnote, gap=8.0)

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:

Stack(Spacer(0.0, grow=True), label, Spacer(0.0, grow=True))

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:

grid([card(*item) for item in items], columns=3, gap=10.0)

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:

Frame(Text("264 bn", size=46.0), height=180.0, valign="middle")

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:

Image("logo.svg", height=34.0, align="right")

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.