Core · guide
Inline layout
Line boxes over Vixen's layout store — inline-block and inline-flex, shrink-to-fit sizing, baseline alignment and vertical-align, and the one invariant an inline formatting context asks the store to give up.
Edit this page on GitHubDocuments
What it is
An inline formatting context is the layout store's fourth algorithm, after flexbox, block and grid. Where those three each arrange a container's children as boxes, an inline formatting context arranges them onto line boxes: items are placed along the inline axis until one does not fit, a new line begins, and everything on a line is aligned vertically against a shared baseline.
It arrives with three new Display keywords and one new property:
| Keyword | Outer display | Inner display |
|---|---|---|
Display.Inline | inline-level | flow |
Display.InlineBlock | inline-level | flow (a block container) |
Display.InlineFlex | inline-level | flex |
CSS Display §2.1 splits display into an outer type — how a box relates to its siblings — and an
inner type, which is the algorithm that runs inside it. The two are genuinely independent, and
that is the whole shape of this feature: inline-flex is the flex algorithm you already have, in a
box that shares its line instead of taking one.
⚠ inline-block exists to not take the whole line, and that is why it was left unmapped for two
plan items rather than aliased onto Block. A block-level box with width: auto fills its
containing block (CSS 2.1 §10.3.3). An inline-level one resolves the same width: auto by §10.3.9's
shrink-to-fit — it is as wide as its contents — and sits beside whatever comes before and after
it. An alias would have looked like support and behaved like a bug.
What it is for
Anything that has to sit in a row with other things and be no bigger than it needs to be: a badge beside a label, a row of chips that wraps, an icon that lines up with the text next to it, a group of inline controls that flow onto a second line when the panel narrows.
Reach for flexbox when you want to control distribution and growth along one axis. Reach for inline when the natural behaviour you want is "these are as wide as their contents, they sit next to each other, and they wrap when they run out of room" — which is a paragraph's behaviour, applied to boxes.
⚠ A container flows rather than stacks when every one of its in-flow children is inline-level.
It is a question about the children, not about the container: the same display: block box stacks or
flows depending entirely on what is in it.
⚠ Mixed content is not an exception to that, it is CSS 2.1 §9.2.1.1. A container holding both
kinds of child stacks — but each run of its inline-level children is wrapped in an anonymous
block box and flowed onto lines inside it, so text, <p>, more text is three block-level
boxes and the two runs each get their lines. An anonymous block box has no node and takes initial
values for everything, so nothing is painted for it and nothing addresses it; all you see is that
the boxes in a run share a line and the container is as tall as the runs made it.
Using it
Set the display keyword on the children; the container needs nothing but block.
using Vixen.Ui.Layout;public static class ChipRow { public static LayoutNodeId Build(LayoutTree tree) { var row = tree.CreateNode(); tree.SetDisplay(row, Display.Block); tree.SetDimension(row, Dimension.Width, StyleLength.Points(200)); // Three chips, each as tall as it says and as wide as it says. Give them no width and // §10.3.9 makes each one as wide as its own contents instead. foreach (var width in new[] { 40f, 60f, 50f }) { var chip = tree.CreateNode(); tree.SetDisplay(chip, Display.InlineBlock); tree.SetVerticalAlign(chip, VerticalAlign.Top); tree.SetDimension(chip, Dimension.Width, StyleLength.Points(width)); tree.SetDimension(chip, Dimension.Height, StyleLength.Points(20)); tree.AddChild(row, chip); } tree.CalculateLayout(row, 200f, float.NaN, Direction.Ltr); return row; }}From a stylesheet, the same three keywords and vertical-align cross the bridge:
.row { display: block; width: 200px; }.chip { display: inline-block; height: 20px; vertical-align: top; }Baselines
A line's baseline is the deepest ascent among the boxes aligned to it, and each box's ascent is the distance from its top edge down to its own baseline. For an atomic inline that baseline comes from CSS 2.1 §10.8.1:
- a box with line boxes of its own uses its last line box's baseline — last, not first, which is why a two-line
inline-blockpushes its neighbours down; - ⚠ unless it clips. A box whose
overflowis anything butvisiblesynthesises its baseline at its bottom margin edge, because content that can scroll away has no business anchoring the line outside it. Cards, badges and chips almost always declareoverflow: hidden, so this branch fires constantly.
The strut
Every line box begins with an imaginary zero-width inline box carrying the block container's own font
and line height (CSS 2.1 §10.8). It is why an empty line is still a line tall, why a short image never
makes a line shorter than the text beside it would be, and why line-height on a container does
anything at all.
⚠ A strut is font metrics and Vixen.Ui.Layout still has no font — but a strut is five numbers,
and only producing them needs one. So <xref:Vixen.Ui.Layout.StrutMetrics> is a computed value
written on the container, exactly as a resolved font-size is, and the layout store stays geometry.
From a stylesheet nothing has to be written at all: UiDocument resolves the element's face and
writes the strut for you.
// A 20-point face with a 16/4 ascent and descent, at `line-height: 30` — so five points of// half-leading on each side, and a strut box of 21 above the baseline and 9 below.tree.SetStrut(panel, new StrutMetrics(21f, 9f, 16f, 4f, XHeight: 10f, SubOffset: 4f, SuperOffset: 8f));⚠ All-zero means "no strut", which is the initial value, and a tree that never writes one lays out
exactly as this store did before the type existed: a line is as tall as the boxes on it, and the five
font-relative vertical-align values below fall back to Baseline.
vertical-align
All nine values are implemented, and the split that used to divide them is now a question about the document rather than about the store:
| Value | State |
|---|---|
Baseline | done — the initial value |
Top, Bottom | done — measured from the line box's edges; they grow the line without moving its baseline |
Middle, TextTop, TextBottom, Sub, Super | done where the container has a strut, and Baseline where it does not |
Offset | done — CSS's <length> and <percentage>, and the one value that needs no strut |
⚠ TextTop and TextBottom align to the font's content area and not to the line box; under
line-height: 2 those differ by half a font size on each side, which is why a strut carries both
pairs of numbers. A percentage vertical-align resolves against the element's own line-height
and is turned into a distance at the stylesheet bridge, because an inline-level box in the layout
store is a rectangle with no line height in it.
text-align
Where the items on a line box sit along the inline axis, which is the half of the CSS property that <xref:Vixen.Ui.Layout.LegacyTextAlign> is not:
tree.SetTextAlign(panel, TextAlign.Center);| Value | State |
|---|---|
Start, End | done — resolved against direction, like every other logical edge |
Left, Right | done — physical, and they do not flip with direction |
Center | done |
justify | refused — dropped at the stylesheet bridge |
⚠ The slack it distributes is the line's, not the container's. A line beside a float has less of it, so a centred line centres in the band the float left rather than in the content box — and negative slack is left alone, so content wider than its line still overflows past the end edge.
⚠ justify is refused rather than aliased, for the reason the five font-relative
vertical-align values are: it asks for the slack to be spread between a line's words, and a text
leaf is one atomic item here. Spreading it between whole inline-level boxes instead would look
convincing and be a different feature. Dropped at the bridge, so the value falls back to Start —
which is where CSS puts a justified block's last line anyway.
⚠ This moves boxes, never glyphs. Text inside a leaf is aligned a layer out, by Vixen.Ui's
TextAlignShift, because that needs the shaped line's width and this store has no font. The two
compose the way CSS says: a centred line box holding a shrink-to-fit leaf whose own lines are
centred inside it.
Fragmentation: when one node is several boxes
Every other algorithm in this store preserves an invariant it never had to state: one node
produces one box. A LayoutResult holds one rectangle, and that is what makes a hundred thousand
nodes four allocations.
⚠ A non-replaced inline box is the exception. CSS Display §2.2 fragments a span that
crosses a line break into one box per line — with the horizontal border and padding drawn at the two
real ends and not at the breaks. A span with box children that wraps is therefore several
rectangles, and asking for them is two calls:
for (var i = 0; i < tree.GetFragmentCount(span); i++) { var (left, top, width, height, ends) = tree.GetFragment(span, i); // `left` and `top` are relative to the span itself, so add them to wherever the walk has // already got to. A box that did not fragment answers 1 and gives you (0, 0, width, height). Paint(absoluteLeft + left, absoluteTop + top, width, height, ends);}ends is a <xref:Vixen.Ui.Layout.LayoutFragmentEnds>: Start, End,
Both, or None for a middle fragment with a line break on either side. The rectangle already
includes the border and padding at whichever ends are real, so a background needs no reference to
the flag; what the flag decides is which vertical border to stroke, and drawing both on every
fragment is what a naive painter does.
⚠ Everything that does not care keeps working unchanged. GetLeft, GetTop, GetWidth and
GetHeight return the union of a fragmented box's rectangles — which is not a compromise but
CSS 2.1 §10.1's own answer, since the containing block of an absolutely positioned descendant of an
inline box is the bounding box of its first and last fragments. GetFragmentCount answers 1 for
every ordinary node.
Limits
⚠ Fragmentation used to be one level deep and now recurses: a span inside a span inside a span
splits at every depth, and each one draws its own two edges at the two ends of its own content while a
continuation line carries neither. A span with an out-of-flow child splits now too: such a child
is positioned by a walk started from the container once every union is final, because a flattened box
is a containing block whose own layout never runs and nothing else would start one there. ⚠ Its
containing block is the union, per §10.1; what it does not get is §10.6.4's static position, which
LayoutAbsoluteChild reads only for a block or flow-root parent — so an un-inset child lands at
the union's inline start rather than after its in-flow siblings. What is still laid out atomically is
a span with a floated child, which is a limit of the walk rather than of the representation and is
written up in Core/Vixen.Ui.Layout.Tests/InlineKnownGaps.txt.
Also absent, each with its reason in the same file: generated ::before/::after boxes, the strut,
white-space, text-overflow: ellipsis, line-clamp and bidirectional reordering.
⚠ A generated box is the opposite direction from fragmentation — a box with no node behind it —
and the fragment arena does not help. Anonymous block boxes were the other half of that direction
and have landed; what a generated box still needs is a style of its own, which is a second style
slot rather than a second rectangle.
⚠ Text is not re-wrapped here. Vixen.Ui's TextLayout already breaks a string into lines across
a font-fallback chain and reaches the store the way every leaf does — as a measure function. This
algorithm treats such a leaf as one atomic item. The cost is stated rather than hidden: an
inline-level text leaf's first line is not shortened to the space left on the line box it lands on.
⚠ That is still true now that fragmentation has landed, and the two were filed as the same blocker
but are not: there is somewhere to put a shortened first line, and the reason it was refused was
never storage.
⚠ And it was not "a second wrapper" either, which this page asserted until #901 was audited: both
routes to a staircase call TextLayout itself rather than a rival. A block-level leaf beside a
float now wraps to a width per line — it asks LayoutTree.ContentBands from inside its own measure
function, and the offset travels on TextLine.Offset, where text-indent already put one. What is
left is this page's own case, the inline-level leaf, and what stops the same query serving it is a
pass order rather than a protocol: this algorithm sizes every item before it breaks a single line,
so at the moment such a leaf is measured there is no line box for it to be shortened to.
Examples
Three inline-level boxes on one line, the middle one a flex container — re-expressed from
web-platform-tests' css/css-flexbox/inline-flex.html:
var first = InlineBox(tree, root, Display.InlineBlock, 50f, 50f);var middle = InlineBox(tree, root, Display.InlineFlex, 50f, 50f);var last = InlineBox(tree, root, Display.InlineBlock, 50f, 50f);// ⚠ CSS's initial flex-direction is `row`; this store's is `column`.tree.SetFlexDirection(middle, FlexDirection.Row);tree.CalculateLayout(root, 300f, float.NaN, Direction.Ltr);// 0, 50, 100 — one line, and the flex container still lays out inside.Boxes of unequal height hanging from a common baseline. Each synthesises its baseline at its own bottom edge, so their bottoms line up — which is why a row of differently-sized badges sits level along its underside:
var short_ = InlineBox(tree, root, Display.InlineBlock, 20f, 10f); // top = 20var tall = InlineBox(tree, root, Display.InlineBlock, 40f, 10f); // top = 0var middling = InlineBox(tree, root, Display.InlineBlock, 30f, 10f); // top = 10Pinning one of them to the top of the line instead, which leaves the baseline where the tall box put it:
tree.SetVerticalAlign(short_, VerticalAlign.Top); // top = 0, not 20See also
- Floats and clear — ⚠ this entry used to say a line box does not shorten as it passes a float. It does:
WalkInlineLinesasks the exclusion list for the band at each line's own top and height. What a line still does not do is break inside a text leaf, so a paragraph that shares a line box with other inline-level boxes re-flows as whole leaves. A block-level paragraph beside a float does step down it, line by line — seeLayoutTree.ContentBands. - Grid layout — the store's third algorithm, and what it cost.
- Utility composition — the
inline,inline-block,inline-flexandalign-*utilities. Core/Vixen.Ui.Layout/README.md— what each algorithm cost the store, in order.Core/Vixen.Ui.Layout.Tests/InlineKnownGaps.txt— every rule that is absent, and why.Core/Vixen.Ui.Layout.Tests/InlineFormattingTests.cs— the WPT-derived oracle.