Layout rules
How the box-layout engine measures, splits and paints nodes, and how that decides where each page breaks.
Measure, split, paint#
The protocol every node implements.
Every element builds a layout node. Nodes know nothing about pages; they answer four questions:
| Method | Answers |
|---|---|
measure(width) | How tall am I at this width? |
split(width, height, fresh:) | The part that fits in height, and the remainder (either may be nil). fresh: true means nothing is above it on a new page. |
natural_width / min_width | Preferred and narrowest widths — how :auto columns and table widths are resolved. |
paint(canvas, x, y, width) | Draw at a position already decided. |
The paginator splits the document's root flow at the height of each page's content box, paints the head on that page and carries on with the remainder until nothing is left. Page templates, headers and footers run afterwards, so they know page.count.
Pagination#
What moves, what splits.
A flow (the document body, and every box's content) stacks children top to bottom. At a page break:
- a splittable child — text, lists, tables, boxes, rows, groups, wraps — continues on the next page;
- anything else (an image, a rule, a canvas, a fixed-height box) moves there whole;
- a
spacerthat lands on the break is dropped; page_breakforces what follows onto a new page;- a float never splits: when it does not fit, or what wraps beside it could not start there, it moves to the next page with that content. A paragraph, a list item and a box beside it split as they do anywhere, and what they carry over is wrapped again at the full width; a box with a background keeps the full width on both pages, the float over it on the first. Floats taller than a page together are cut before the first that does not fit, and only a float taller than a page by itself is reported as an
Overflow.
Text splits between lines. Tables split between rows (never through a rowspan) and repeat their header rows. Lists keep each marker with the first line of its item.
keep_with_next#
Never end a page on a heading.
On text, box or group, keep_with_next: true never ends a page with that node: it moves to the next page with its successor. A number keeps at least that many points of what follows with it:
text "3. Financial review", size: 16, weight: :bold, keep_with_next: 60
table(rows, header: true)A kept node also moves when its successor avoids breaking inside and would not start on the page.
Page breaks in text#
orphans and widows.
A paragraph splits between lines. orphans: is the fewest lines a break may leave at the foot of a page and widows: the fewest it may carry to the next; both default to 1, so any line may end or start a page. With larger values the break moves up until both are met, and a paragraph that cannot meet them (it would leave too few lines behind, or is shorter than orphans plus widows) moves to the next page whole. A paragraph that already starts a fresh page cannot move, so it splits as best it can, keeping the widows if a line can still stay.
default_text orphans: 2, widows: 2 # for the whole document
text long_story, orphans: 3, widows: 3 # or per paragraph
html body, styles: { p: { orphans: 2, widows: 2 } }text_style sets them for a block like any text default.
break_inside#
Soft avoid, auto, avoid.
break_inside: is accepted by box, row, column and text:
| Value | Behaviour |
|---|---|
nil (default, "soft avoid") | Moves to the next page whole when it fits there; continues across pages when it does not fit on a page of its own. |
:auto | Splits at any page break, wherever it starts. |
:avoid | Never splits. Content taller than a page overflows (and warns). |
group(keep_together: true) is break_inside: :avoid for a group.
Decoration at a cut#
slice or clone.
When a box splits, each fragment's cut side is open. decoration: :slice (default) drops the padding and border on that side, like CSS box-decoration-break: slice; :clone keeps the padding. Borders are left open and rounded corners are cut square at the break.
box(background: "#F0FDFA", padding: 16, radius: 8, break_inside: :auto, decoration: :clone) do
text long_callout
endRows split every column at the same break; a column that ends early continues as an empty fragment so backgrounds stay aligned.
Fixed heights and overflow#
truncate or shrink_to_fit.
A box with height: never splits, and neither does a row holding one. When its text does not fit, overflow: decides:
:visible(default) — draw it anyway;:truncate— keep the whole lines that fit and drop the rest;:shrink_to_fit— reduce the text size until it fits (when the box holds a singletext; otherwise it truncates).
box(height: 40, overflow: :shrink_to_fit, valign: :middle) { text customer.name, size: 18 }Overflow warnings#
Content is placed anyway.
Content taller than the space a page has for it — an :avoid box, an image, a fixed-height box — is placed anyway and never raised on. Each case is recorded as a Stationery::Warnings::Overflow with the page, the height placed and the height available:
doc.to_pdf
doc.warnings.map(&:message)
# => ["content 912.0pt tall placed on page 3 with 770.0pt available"]Use strict to fail instead — see Warnings and strict mode — and to_pdf(debug: true) to see the rectangles involved.