This page describes the front half of the pipeline as built: the styled-node tree, computed values, the box tree and the fragment tree. Layout algorithms, paint and the display list build on it.
OneHtml Document + DocumentStyles ──► HtmlFrontEnd ─┐ ├─► StyledTree ─► BoxTree ─► ILayoutEngine ─► FragmentTree C# builder (StyleDeclaration) ──────────────────────┘ (+ StyleTable)
Shared rules
- Handles.
StyledNode,BoxandFragmentarereadonly struct { Tree, Index }. They hold no data; every accessor reads a column of the owning tree. The default value is the null handle (IsNull). A handle is valid until its tree is cleared or rebuilt; indexes are dense, assigned in document order (parents before children). - Storage. Each tree is struct-of-arrays: parallel
int/byte/doublecolumns and first-child / next-sibling links, grown by doubling and retained acrossClear()/Build(). Rebuilding a tree of the same size allocates nothing (asserted by tests for a 1,000-node document built from HTML and from the builder). - No recursion on depth. Every traversal (building, dumping, layout helpers) uses an explicit stack. Trees of 100,000 levels or 100,000 siblings build and dump.
- Limits.
TreeLimitsbounds node count (2M), depth (1,048,576) and text length (32M UTF-16 units); the box tree allows twice the node limit. A limit is checked before anything is stored; the over-limit node is dropped and aLimitExceededentry goes toStyledTree.Diagnostics(capped at 256 entries, the rest counted inDropped). Declarations that survive the cascade but do not parse raiseInvalidValuediagnostics. - Determinism. Ordinal comparisons only, no culture-sensitive APIs, no clock, no file or network access. The host
supplies natural sizes of replaced content through
IResourceResolverand fonts throughIFontProvider.
Computed values (OneRender.Style)
PropertyIdlists the longhands the engine models (display, box model, position, sizing, borders, overflow, font, text, color and a few more). Shorthands are expanded before they reach the pipeline: by OneHtml's cascade for the HTML front end and byStyleDeclaration.TrySetfor the builder, withShorthandFallbackscoveringborder-radius,overflow,flex,gridand the others the cascade keeps whole. (white-space,text-wrapandborder-block/border-inlineare expanded by OneHtml 1.2 on both paths.)ComputedValuesis one struct of typed fields (enums,Length,ColorValue, doubles). Styles are interned: aStyleTablestores one row per distinct computed style, and a node holds aStyleId. A document with thousands of nodes typically has tens of rows. Rows are found by value equality, soStyleIdequality means identical style. The table holds at most 65,536 rows at once; rows nothing refers to any more are freed and reused (see Long-running stability). If a single flush needs more than the table can hold, a diagnostic is raised and the parent's style is reused.- A computed style is cached by (declaration source, version, parent
StyleId, root font size); a warm rebuild computes nothing and allocates nothing. Lengthkeeps what layout needs: pixels for every absolute, font-relative and viewport unit, and an unresolved form for percentages andcalc()containing percentages (CalcValueevaluates against a containing-block size at layout time). Keywords (auto,min-content,max-content,fit-content,normal,none) areLengthKindvalues.inherit,initialandunsetare resolved when the style is computed.revertandrevert-layeract asunsetin builder trees, which have no earlier origin.- Invalid declarations are dropped in the cascade.
StyleValidation.Validatoris the engine's grammar offered to OneHtml asStyleSourceOptions.DeclarationValidator(HtmlPagesets it; a host that builds its ownDocumentStylesshould too). OneHtml asks it once per declaration, when a rule's declarations are first collected, for author and user declarations of properties OneHtml does not type itself. A rejected declaration neither wins nor counts as!important, so an earlier valid one stands (CSS Syntax 3 section 5.4.6, CSS Cascade 4 section 4.3). For a longhand the validator runs the same parser that computes the value, against initial values, so the two cannot disagree; forborder-radius,overflow,flex,gridand the other shorthands OneHtml passes through whole, it expands them withShorthandFallbacksand checks every longhand. The check parses the value text again (OneHtml's token range cannot be turned back into a value list), which costs one small allocation per declaration per stylesheet and nothing per restyle. It holds one lock around a shared scratch computer. - What still reaches the style computer as invalid behaves as
unsetand raisesInvalidValue(CSS Variables 1 section 3.2 forvar()): values withvar(), which are only known at computed-value time; properties OneHtml types itself (display,position, the offsets,overflow-x/y,visibility,font-size,font-weight,font-style,font-family), where OneHtml's own grammar decides; and builder trees, which have no cascade. - What is deliberately lossy is listed, with the reason, in
tests/OneRender.Tests/testdata/wpt-style/expected-failures.tsv(colors are stored as sRGB,contentwith counters is kept as raw text, and so on).
Styled-node tree (OneRender.Tree.StyledTree)
Elements and text nodes in document order, each with a tag name (builder trees and diagnostics), a StyleId and, for
the HTML front end, the Node.Index of the OneHtml node it came from (StyledTree.FindBySource). Two front ends feed it:
HtmlFrontEnd.Build(DocumentStyles)walks the OneHtml document with an explicit stack, visits elements and text (not comments), computes styles from the cascade, and adds::before,::afterand::markernodes from OneHtml's pseudo-element styles when they generate content. Elements the user-agent sheet hides (head,script,style) aredisplay: noneand get no box.- The builder:
AddRoot/AddElement/AddTextwith aStyleDeclarationof CSS text per node (new StyleDeclaration().Set("width", "50%")). It goes through the same value parsers as a style sheet.
Replaced elements (img, video, canvas, iframe, embed, object, svg) have no children in the styled tree.
Their natural size comes from IResourceResolver (or StyledTree.SetNaturalSize in a builder tree); with none, 300x150
for non-images and NaN for images.
Box tree (OneRender.Tree.BoxTree)
BoxTree.Build() generates boxes from the styled tree following CSS Display 3 and CSS2 §9.2.
- A
Boxhas aKind(Container, Inline, Text, Replaced, LineBreak, Marker), an outerLevel(Block or Inline) and, for containers, aFormattingContext(Block, FlowRoot, Flex, Grid, Table and its parts). Flex, grid and table boxes are marked only; table internals are not fixed up. display: nonemakes no box;display: contentsmakes the element's children boxes of its parent.- Anonymous block boxes wrap inline runs when a container also has block-level in-flow children (§9.2.1.1), and anonymous inline (text) boxes wrap text (§9.2.2.1). Out-of-flow boxes and outside markers do not split or start runs.
- A block inside an inline splits the inline into pieces linked by
Box.NextPiece(Continuationmarks the later ones); nesting deeper than 64 turns the block into an inline-block and raises a diagnostic. - Whitespace-only text that collapses is dropped at run edges, in flex and grid containers and inside table structure (CSS Text 3 §4.1.1). Children of flex and grid containers are blockified; their text runs get anonymous items.
list-itemboxes get aMarkerbox (a placeholder; none whenlist-style-typeisnone).BoxTree.PrincipalBox(StyledNode)maps a styled node to its (first) box.BoxTree.Dump()prints an indented text form, used by tests to compare the two front ends.
Fragment tree (OneRender.Layout)
Layout writes a FragmentTree and paint, hit testing and the harnesses read it.
- Kinds:
Box(a box fragment, one per box or per split piece),Line(a line box inside an inline formatting context),Text(a run of text in one line, referencing a glyph run) andColumn(a fragmentainer, see "Fragmentation"; it has no box). - Coordinates. Every fragment's
BorderBoxis in CSS px, relative to the border-box origin of its parent fragment; the root's is relative to the initial containing block.AbsoluteBorderBoxandAbsoluteOriginwalk up the chain.BorderandPaddingare the resolved edge widths; the content box follows from them.ScrollableOverflowis in the fragment's own coordinates and covers the box's descendants' overflow;Baselineis measured from the border-box top. - Glyph runs.
AddGlyphRunstores a OneFontFontFace, the font size, glyph ids, x/y positions and cluster indexes (into the box's text) in shared columns; aTextfragment holds aGlyphRunId. - Fragmentation. A box that splits (inline pieces, later fragmentainers) has several fragments chained by
Fragment.NextOfBox;FragmentOf(box)returns the first.Fragment.FragmentainerIndexis the index of the column a fragment landed in (0 outside any fragmentation context). - Keyed by element.
TryGetBorderBox(StyledNode, out Rect)andTryGetBorderBox(Node, out Rect)(a OneHtml node) return the union of the element's fragments in document coordinates; this is what the geometry differential compares. FragmentTree.Dump()prints the tree as text.
The layout entry
public interface ILayoutEngine { void Layout(BoxTree boxes, in LayoutInput input, FragmentTree output, CancellationToken cancellationToken = default); }
LayoutInput carries the initial containing block (AvailableWidth, and AvailableHeight or NaN when the content
decides) and an IFontProvider, the text service layout calls: Resolve(in ComputedValues) matches a style to a font list with
per-character fallback, Shape(text, in ComputedValues, rtl) returns a cached ShapedText, and GetMetrics(in ComputedValues)
returns the primary face's metrics (see text.md; TextFontProvider is the engine's implementation). An implementation clears output, writes a fragment for the root and for every box that generates one in tree
order, reads nothing but its arguments, and checks the token every few hundred boxes. The result is a pure function of
the box tree, the fonts and the available size. Block and inline flow, floats, positioning, flex, grid and tables are separate implementations behind this one entry; fragmentation is a
stage the block engine runs at the end (see "Fragmentation").
Block layout (BlockLayoutEngine)
BlockLayoutEngine implements ILayoutEngine for normal-flow block-level boxes: the block formatting context of CSS2
§9.4.1, widths (§10.3.3, replaced §10.3.2/§10.3.4 with the constraint table of §10.4), heights (§10.5 percentages that
behave as auto, §10.6.3, min/max §10.7), box-sizing, aspect-ratio (Sizing 4 §5), margin collapsing (§8.3.1) and
intrinsic sizes (Sizing 3 §5). Grid and table containers are laid out as plain block containers until their own
layout exists (flex containers: see "Flex layout"); floats and clear are described under "Floats" below; absolutely positioned boxes are described under "Positioning".
- No recursion. One explicit stack of frames (a struct array kept between runs) walks the box tree; a frame is a box whose children are being placed. Depth is capped at 262,144 levels (a diagnostic, and the deeper boxes get no fragment). Laying out an unchanged tree again allocates nothing.
- Coordinates. Every fragment is added when its box is opened, with its border-box rectangle fixed when it closes. Child
rectangles are relative to the parent's border box, as the fragment tree requires.
Baselineof a block is its first in-flow line or child baseline from the border-box top, NaN when there is none. - Margin collapsing. One margin run (largest positive plus most negative) is kept for the block formatting context.
A box whose top margin adjoins its first child's (no top border or padding, not a BFC root) is pending: the run keeps
growing through the chain of pending boxes and, when the first thing with content (or a box with its own top border or
padding, or a BFC root) is reached, it is resolved once and moves the outermost pending box; nested pending boxes sit at
offset 0 in their parent. A pending box that closes without content collapses through: it sits where a box with a
non-zero bottom border would (§8.3.1), and its bottom margin joins the run. At the end of a box the run passes through to
the parent only if the height is auto, nothing is below the last child, the box is not a BFC root, and min/max/aspect-ratio
do not change the height; otherwise it is resolved into the content height. Boxes that establish a BFC are the root
element, anything but
display: block/list-item/anonymous containers, floats, absolutely positioned boxes and scroll containers (overflowother thanvisible/clip). - Intrinsic sizes (
GetIntrinsicSizes) are content-box min-content and max-content widths, cached per box until the nextLayout, computed with an explicit stack. A block's sizes are the largest child contribution (the child's definite width or intrinsic size, clamped by min/max width, plus fixed margins, border and padding). They backmin-content,max-content,fit-contentand shrink-to-fit widths. Percentages count as auto. - Re-entry.
LayoutIndependentlays out one box as its own BFC root, nested inside a running layout; inline layout uses it for inline-block and other atomic boxes (shrinkToFitselects CSS2 §10.3.5 widths). - List items. An outside
::markerbox gets a fragment onefont-sizewide and one line high just outside the item's start edge, at the content top. This is a placeholder until real markers. - Not yet: the block-axis
fit-content.
The inline seam (IInlineLayout)
A block container with ChildLayout.Inline hands its children to an IInlineLayout:
InlineLayoutResult Layout(in InlineLayoutRequest request, FragmentTree output, Fragment parent, CancellationToken ct); void Measure(Box container, IFontProvider fonts, out double minContent, out double maxContent);
- The request carries the container, the content-box width to fill, the content box's offset inside the container's border
box (
ContentLeft,ContentTop, where lines are added as children ofparent), the content box's origin in the BFC root's content box (BfcLeft,BfcTop, where float exclusions are looked up), the font provider and the exclusion space (Exclusions, see "Floats"). A request built without one has the emptyNoExclusionsspace. - The result is the total line height, the line count and the first and last baselines from the content top. A container with lines counts as having content for margin collapsing; the block layout settles the margin run before the call.
Measurereturns the content-box min-content and max-content widths.- The result also carries the extent of everything the lines occupy (
HasOverflow,Overflow*, in the container's border-box coordinates), which the block layout adds to the scrollable overflow, andQueuedAbsolutes, true when the inline layout has already handed the absolute boxes of its content to the block layout (IAtomicInlineHost.QueueAbsolute). InlineLayoutEngine(see "Inline layout") is the default.PlaceholderInlineLayoutis the stand-in for tests of the block layout that need no fonts: a container with any non-blank text, replaced or atomic inline child gets one empty line box as tall asline-height(normalis 1.2 × font size) and the intrinsic inline sizes are 0.
Floats
Floats, clear and the boxes that avoid floats live in Layout/Block/BlockLayoutEngine.Floats.cs and .Open.cs.
- Placement (CSS2 §9.5.1). Each block formatting context keeps its floats as margin boxes in the coordinates of its root's
content box, sorted by top (rule 5 makes the tops non-decreasing). A float is laid out first, as the root of a context of
its own, so its size is known; its position is then the highest one (rule 8) that is as far left, or right, as the earlier
floats allow (rules 2, 3, 7 and 9), not above the flow position it was met at, the top of its containing block or the line
it sits on (rules 4 to 6). Rule 3 is measured against the other side's floats wherever they are; rule 7 applies whenever
another float on the same side is in the way, even outside the containing block, and a float with none to its side may stick
out of its containing block.
float: inline-start | inline-endand the logicalclearvalues are resolved to left and right by the computed style. - Width and height. A float's
autowidth is shrink-to-fit (§10.3.5, from the intrinsic-size cache) and its auto margins are 0; a floated replaced box uses §10.3.6. The auto height of a block formatting context root reaches the bottom margin edge of its lowest float (§10.6.7); floats are also added to the root's scrollable overflow. Intrinsic sizes count floats (Sizing 3 §5.2): the widest float for min-content, and the sum of consecutive floats (broken byclear) for max-content. - Floats and margins. A float is placed where a block would be, after the margins collected so far. While the top margin of the box that holds it is still pending (§8.3.1) the float is held until that position is settled, so that a larger margin collapsing through the container moves the float with it. A box that starts a formatting context is separated from adjoining floats: a margin that would pull them down does not.
- Clearance (§9.5.2). A box with
cleargets clearance when its hypothetical position, with its own top margin and the margins of the first children that adjoin it collapsed into the run, is above the bottom edge of the floats it clears, or when a float adjoining it would otherwise be pulled down with it. The margins before the box end there; its border edge goes to the bottom edge of the floats whatever its own margin is. A cleared box that is still pending keeps the margin collapsing with its first child, and its position is raised to the floats' edge when the pending chain is settled. - Boxes beside floats (§9.5). An in-flow block that is a block formatting context root, and block-level replaced boxes,
are put beside the floats at their top edge. An
autowidth shrinks to the room between the floats (not below its min-content width); a definite width fits when its border box and start margin do (an over-constrained end margin is dropped). If the box does not fit it moves down to the next float edge, and with no float edge left it stays. - The exclusion space.
IExclusionSpace, reached throughInlineLayoutRequest.Exclusions, is what inline layout asks:Query(bfcTop, height, bfcLeft, bfcRight, out start, out end, out nextEdge)returns the usable inline range of a band (floats whose margin box overlaps the band; with height 0 the single line atbfcTop) and the smallest bottom edge among them, the next y where more room can appear (+Infinitywhen no float intersects).start == endmeans no room.PlaceFloat(request, container, floatBox, minBfcTop, out marginBox)lays out and places a float met in inline content.minBfcTopis the top of the current line when the float fits on that line and the line's bottom when it does not; the inline layout decides, which is the hook for rule 6 and 7 of §9.5.1. The float's fragment is added to the container's fragment and the margin box comes back in block-formatting-context coordinates.PlaceholderInlineLayoutplaces the floats of its container at the top of the content box, then puts its one line in the room beside them, below the next float edge when there is none.
- Fragments. A float's fragment is a child of its containing block's fragment, in tree order; painting orders it (Appendix E step 5).
- Bounds. No recursion on tree depth: floats in the block flow are frames of the same explicit stack (nested floats need no call depth), and floats in inline content nest one engine call each, at most 32 deep (then a diagnostic and no fragment). A context stops searching after 50 million float visits or 262,144 floats: later floats are stacked below everything placed so far, lines ignore floats, and a diagnostic says so. Placement only scans the floats that can still matter (those ending below the lowest float top), so rows of thousands of floats cost about linear time. The margin-collapse look-ahead for clearance follows at most 64 first children. All storage is kept between layouts.
- Chosen where the specs leave room. Floats in a settled position use the margins collected so far (matching what browsers do); a cleared box's border edge ignores its own margin when clearance applies; the 'separated from adjoining floats' rule for formatting-context boxes uses the smaller of the two outcomes (pulled down or not).
- Checked by.
FloatLayoutTests(spec examples by fragment border boxes, allocation and limits) andWptFloatReftests: 58 CSS2floatsandfloats-clearreftests whose page and reference render to the same pixels throughHtmlRender, with the whole-pixel line height injected. The test withONERENDER_FLOAT_SWEEPset sweeps the whole directories of a web-platform-tests checkout and writes one result per test.
Inline layout (Layout/Inline/)
InlineLayoutEngine is the IInlineLayout behind BlockLayoutEngine(). It lays out one inline formatting context (CSS2 §9.4.2,
CSS Text 3, CSS Inline 3) per call, as one paragraph, and keeps one worker per nesting depth because atomic inlines
re-enter it through IAtomicInlineHost (at most 40 deep; deeper content is dropped). Everything is stored in arrays that grow and
are reused, so a relayout of unchanged content allocates nothing.
- Collect. An explicit-stack walk of the container's subtree produces the paragraph text and raw elements: text runs, inline
box open and close, atomic inlines (U+FFFC in the text), tabs, forced breaks, floats and absolute boxes. White space is
processed here (CSS Text 3 §4.1, every
white-space-collapseandtext-wrap-modevalue): collapsing crosses inline box boundaries, segment breaks andtab-sizefollow the mode, and each character keeps a flag for "collapsible", "word separator" and the source offset it came from.unicode-bidiembeddings, isolates and overrides become the UAX #9 controls. - Prepare. UAX #14 opportunities and grapheme starts come from OneFont;
AdjustOpportunitiesturns them into CSS's:white-space,word-break(normal, break-all, keep-all, break-word),line-break: anywhere, soft hyphens (hyphens: manualonly;noneremoves them), thebreak-spacesrule, and breaks around atomic inlines. Bidi levels come from UAX #9; a paragraph with a left-to-right base and no character at U+0590 or above skips the algorithm (all levels are 0). Text is split by level and into pieces of at most 1,024 characters (at a break opportunity or grapheme boundary) and shaped throughIFontProvider.Shape, whose cache is keyed by text and style, so an unchanged paragraph hits it without allocating (a page with more than about 4,096 distinct runs thrashes that cache and allocates on a relayout). - Scan.
ScanLineis the greedy breaker: it accumulates clusters, remembers the last opportunity that fits, rolls back to it, hangs trailing collapsible and preserved spaces (CSS Text 3 §4.1.3), and breaks inside a word only when the line has no opportunity (overflow-wrap: break-word | anywhere). Min-content and max-content (Measure) run the same scan with an available width of 0 and infinity; a forced break splits max-content, atomic inlines count whole,anywherecounts in min-content andbreak-worddoes not. - Lines. For each line the exclusion space is asked for the usable range at the line's top and height; a line whose first
unbreakable item does not fit moves down to the next float edge. Floats met in the content are placed with
PlaceFloat: at the line's top when the content before the float plus its shrink-to-fit width fits on the line, below the line otherwise (CSS2 §9.5.1 rule 6); the line is scanned again when a float joined it. The line's height needs the vertical metrics of its pieces, and the strut is always part of it (§10.8.1). - Vertical metrics. Each inline box has ascent, descent and half-leading from its font and
line-height;vertical-alignshifts it (baseline, sub, super, text-top, text-bottom, middle, lengths and percentages of the line height; top and bottom align to the line box after the others are placed). An inline-block's baseline is the baseline of its last in-flow line, or its bottom margin edge whenoverflowis notvisibleor it has no line. The first baseline is stored on the container's fragment (SetBaseline); an inline-block's last baseline is found by walking its fragments. - Emit. Alignment (
text-align,text-align-last,text-indentwithhangingandeach-line, justification between words,letter-spacing,word-spacing), then bidi reordering of the line's pieces (UAX #9 L2), then fragments: aLinefragment per line; inside it oneBoxfragment per run of consecutive pieces of an inline box (so a box split across lines, or across a nested bidi run, has several pieces chained withNextOfBox, withContinuesBefore/ContinuesAfter, and its border and padding only on the first and last piece,box-decoration-break: slice); inside those oneTextfragment per font face of each piece with a glyph run (glyph ids, positions, source offsets). Atomic inlines are laid out through the host while preparing and moved into the right fragment withFragmentTree.Reparent.TryGetBorderBoxof an inline element is the union of its pieces. - Bounds. No recursion on tree depth (a 10,000-deep span nest is walked with the explicit stack). A paragraph holds at most
4,194,304 characters (the rest is dropped); line scanning checks the cancellation token every 1,024 steps; a line that cannot
place anything still consumes one cluster, so there is no endless loop. Costs on the development machine: a 1,000,000-character
paragraph (12,171 lines) lays out in about 0.3 s including the document build, a 100,000-character unbreakable word in about
25 ms (one line) or 30 ms with
overflow-wrap: anywhere(4,000 lines), a 10,000-deep inline nest in about 30 ms. All are linear: shaping is per piece of at most 1,024 characters and a line is scanned once, plus once more for each float that joins it. - Choices where the specs leave room (each checked against Chrome by the
inline-labpages, where 98.7% to 99.4% of the elements are within the geometry tolerance):- Ascent, descent and line gap are rounded to whole pixels (half away from zero) before half-leading is computed; the half-leading's top is the floor of half the leading and the bottom takes the rest. This put the lab from 9% to 90% within tolerance.
sublowers by a fifth of the parent's font size plus 1 px andsuperraises by a third plus 1 px.- An inline box's open and close edges take the bidi level of the next/previous content, so the box's pieces stay contiguous. Known deviation: an RTL-direction box around left-to-right text puts its start edge on the wrong side.
text-justify: inter-characteris treated asinter-word;line-break: strict | loose | normalare alike; letter-spacing does not disable ligatures; a percentageword-spacingis a fraction of the separator's advance.- An empty inline box alone on a line produces no fragment (Chrome gives a zero-size box).
- Percentage
line-height, andemlengths ofletter-spacing,word-spacing,text-indentandtab-size, inherit as the ancestor's computed length (CSS Values 4 §7.1), not as text resolved against the descendant's font.
- Not done:
::first-lineand::first-letter,text-overflow, line clamping,hyphens: auto,text-decorationand text shadows, vertical writing modes, ruby,box-decoration-break: clone, inline-flex, inline-grid and inline-table as anything but blocks, markers inside the line, andinitial-letter. - Checked by.
InlineLayoutTests(a section per topic, the 0 B relayout of a 1,000-box text page and the limits above); theinlinefuzz target; theinline-labdifferential pages intestdata/inline-lab/.
Positioning (BlockLayoutEngine.Positioned.cs)
- Containing blocks (CSS2 §10.1, Position 3 §2). Each block frame carries
CbFrag, the fragment of its nearest positioned ancestor (positionother thanstatic), or -1 for the initial containing block. Nested layouts started throughLayoutIndependentinherit it from the frame that started them. A positioned inline box is a containing block too (§10.1 item 4): inline layout passes the fragment of the nearest positioned inline box around an absolute box as that box's containing block. When the inline box is split across lines this is the piece that holds the absolute box, not the union of the first and last pieces. - Relative (§9.4.3). The shift is applied when a box closes, to the fragment rectangle only: the flow position that siblings
and the parent's height see is unchanged. A shifted box adds its shifted rectangle to the parent's scrollable overflow.
leftwins overrightin a left-to-right containing block,topoverbottomalways; percentagetop/bottomagainst a height that depends on content areauto. - Absolute and fixed (§10.3.7, §10.6.4; replaced §10.3.8, §10.6.5). While placing the in-flow children of a block, an
absolute child is not laid out. It gets a placeholder fragment, appended as the next child of its parent's fragment so
fragment order is document order (paint order needs that), and a queue entry holding its static position in the parent's
border-box coordinates. The static top is the cursor plus the pending margin run (0 when the parent's own top margin is still
pending). After the whole tree is done the queue is resolved in discovery order, so a box is resolved after the box that contains
it:
- document-space origins of all fragments are computed in one pass (parents precede children), so a containing block's padding box and a parent's offset cost O(1) however deep they are;
SolveAbsoluteAxissolves one axis (start + margins + size + end + padding/border = containing size) withautoas NaN: every case of §10.3.7, and §10.6.4 with the vertical differences (static top, equal margins even when negative, bottom ignored when over-constrained); the containing block's direction picks the ignored inset. Min/max apply as §10.4 says, by solving again with the violated size. Anautowidth is shrink-to-fit from the intrinsic sizes; anautoheight is the content height (laid out once with the solved width forced into the box) or follows fromtop/bottomoraspect-ratio;- the box is laid out through the same entry as every independent block (
LayoutIndependent) with the solved sizes forced, into the placeholder, then moved to its place. Percentages of the box itself resolve against the containing block's padding box. Fixed boxes use the viewport (LayoutInput, at scroll position 0) as their containing block; painting counters the viewport scroll, so layout needs no scroll input. The root element can itself beabsolute/fixed; it is then placed in the initial containing block.
- With floats. An absolute box inside a float (or in any nested formatting context) is queued like any other: the float lays its content out in its own context, and the box's static position is taken from the float's frame. Resolution runs after every float of the document is placed, and lays the box out in a formatting context of its own, so it is neither pushed by nor does it push floats. A relatively positioned float is placed as a float first and drawn shifted afterwards; the exclusion it leaves for lines and later floats stays where the float was placed, and its shifted rectangle adds to the scrollable overflow.
- Absolute boxes inside inline content. The inline layout reports each one with
IAtomicInlineHost.QueueAbsolute: the static position is the inline position reached on the line when the box is met, at the top of the line box. A box with nothing but out-of-flow boxes before it on its line is at the start edge of the container's content box, however floats narrow the line. An inline layout that does not report (the placeholder) leaves the box at the start of the content box (QueueInlineAbsolutes). - Relatively positioned inline boxes are drawn shifted by their offset; the line and the box's content move with the fragment and the line is unchanged.
- Sticky (Position 3 §3.4) takes no part in layout (it is in flow like
relativewith no offset). The shift is computed at paint time, see below. - Bounds. The queue is capped at 4M boxes, nothing recurses, the resolution pass is linear in fragments plus queue entries, and the token is checked every 64 boxes. A 3,000-deep nest of positioned boxes with an absolute child at each level lays out in milliseconds (test).
Overflow and scrolling
- Overflow values (Overflow 3 §3.1) are already computed with the visible/clip → auto/hidden rule when the other axis scrolls.
A scroll container is a box with
overflowother thanvisible/clipon an axis;overflow: clipclips likehiddenbut is not scrollable.overflow-clip-margin(Overflow 4 §3.2:<visual-box> || <length>, padding-box and 0 by default) outsets the clip edge onclipaxes only. - Viewport propagation (Overflow 3 §3.3).
ViewportOverflow.Resolvepicks the root'soverflowor, when the root ishtmlwithvisibleand has abodychild with something else, the body's;visiblebecomesautoandclipbecomeshiddenfor the viewport. The element it came from neither clips, nor is a scroll container, nor establishes a block formatting context for that reason. Layout and the painter both ask the same function. - Scrollable overflow (Overflow 3 §2.2) is recorded on every box fragment (
Fragment.ScrollableOverflow, in the fragment's border-box coordinates). It starts from the border box and takes in-flow children's overflow (shifted by relative offsets). A box that clips an axis contributes only its border box on that axis. A scroll container's own area starts at its padding box, and content that reaches past the end of the content box also brings the end padding in. Absolute boxes add their overflow to the fragment of their containing block, not their parent, and from there to every ancestor that does not clip; fixed boxes add nothing. This is one pass over the fragments in reverse order after the queue is resolved. - Scroll positions are host input in a
ScrollState: one viewport offset and one offset per scroll container (keyed by box index, so callClear()after rebuilding the box tree). They are stored as requested and clamped when used to whatScrollState.GetRange(a container) orGetViewportRange(the root's scrollable overflow beyond the viewport) allows:0..scrollable overflow − padding box, and fordirection: rtlcontainersoverflow start − padding start .. 0.HtmlPage.Scrollis the page's state. Layout never reads it. - Painting scrolls.
PaintOptions.Scrollturns it on. A scroll container that can scroll emitsPushClipRect(fixed) and, inside it, aPushTransformthat translates its content by the negative offset; the viewport gets one around everything but the canvas; fixed boxes get a counter-translation by the viewport offset; sticky boxes get a translation of their own. A box painted outside its containing block's scope (an absolute or relative box deferred to its stacking context) gets a copy of the scroll transform of each scroll container on its containing-block chain, so it scrolls with the container but not with ancestors it escapes. Items are only emitted when the container can scroll, which depends on layout, never on the offset. Their ids use parts 6 (scroll container), 7 (sticky), 8 (fixed counter-scroll) and 9 (copy of a scroll transform pushed for a deferred box), andItemId.Of(0, 6, 0)for the viewport. - Changing a position never repaints.
Painter.PatchScroll(retained)recomputes the translation of every scroll, viewport, fixed and sticky item the last paint emitted and callsPatchTransformon those that changed; the damage is the old and new bounds of what moved, clipped by the container, and nothing allocates. The patched list equals a fresh paint (tested by comparing ids, bounds and both hashes). The sequence for a host is:page.Scroll.SetOffset(box, x, y),painter.PatchScroll(retained), draw the damage. - Sticky shifts are
StickyPositioning.Offset(borderBox, margins, containingBlock, scrollport, scroll, insets), a pure function: on each axis the box is moved inward of the scrollport edge by its inset (the scrollport shifted by the scroll offset, in unscrolled coordinates) and then limited so its margin box stays in its containing block (the parent's content box); with both insets on an axis, the start inset wins. The painter records the geometry in document space when it paints, so a scroll change is arithmetic on a table. A sticky box inside another sticky box is shifted relative to the outer one's final position. The scrollport of a sticky box is its nearest scroll container's padding box, or the viewport. - Not done: scrollbars (no gutter, nothing is drawn), scroll snapping, scroll events,
scroll-behavior,overscroll,contain, and the overflow of the content of an atomic inline (the border box is used). - Overflow of inline content. Line boxes, inline box edges, text that runs past the end of its line (an unbreakable word, a
nowrapline) and atomic inlines add to the scrollable overflow of the container (InlineLayoutResult.Overflow*).
Flex layout (Layout/Flex/, BlockLayoutEngine.Flex.cs)
Flexbox Level 1 §9 on the block engine's frame stack, with Box Alignment 3 for justify-content, align-content, align-items,
align-self (including safe/unsafe) and gap. Block-level and inline-level flex containers are both handled; the container
is a formatting-context root like a BFC root, so floats stay outside it and margins do not collapse through it.
- Style.
flex-direction,flex-wrap,flex-grow,flex-shrink,flex-basis,order, the alignment properties androw-gap/column-gapare typed inComputedValues. The cascade layer does not expandflex,flex-flowandplace-*, soShorthandFallbacksdoes, with the Flexbox §7 rules (flex: 1is1 1 0,flex: autois1 1 auto,noneis0 0 auto). - Phases per container. Init (collect in-flow items in
order, skippingdisplay: none; absolutely positioned children are queued for the static position), Base (flex base size and hypothetical main size per item, §9.2), Lines (§9.3), Resolve flexible lengths (§9.7, with min/max violation freezing; capped at 256 rounds), Cross (§9.4 hypothetical cross sizes, line cross size,align-content, stretch), Final (§9.5 main-axis placement, §9.6 cross-axis alignment, §8.3 baseline alignment) and Close. - Trial layouts. Measuring an item means laying it out at a forced border-box width and/or height (
ForceW/ForceH), then truncating the fragments it produced (FragmentTree.Mark/Truncate). The measurement is memoized per box by the forced sizes, the containing block size, and the containing height only when the subtree has percentage block sizes. A trial-mode container skips its own final phase, so a chain of nested flex containers is measured in linear time. The exact item layout happens once. A budget of 1M + 64 per box trials guards hostile input; past it a diagnostic is recorded and sizes come from style only. min-width/height: auto(§4.5) uses the smaller of the content-size suggestion and the specified size suggestion, clamped bymax; for aspect-ratio items the transferred suggestion applies. Column containers compute the content-size suggestion with a trial at the resolved width.- Intrinsic sizes (§9.9) are in
FlexIntrinsic.csand feed the shared intrinsic cache: single-line containers sum item contributions along the main axis, multi-line take the largest for min-content, and gaps are added. - Absolutely positioned children (§4.1) use the flex container's content box as the containing block and are aligned with
justify-content/align-selfwhen both insets on the axis areauto. - Choices where the specs leave room (to be checked against the Chrome differential once text lands): overflow alignment
defaults to
unsafe;space-around/space-evenlywith negative free space fall back tosafe centerandspace-betweentoflex-start;align-content: normalbehaves asstretch;baselinein a column container falls back toflex-start;place-items/place-selfignore the justify half (it has no effect on flex items);floatandclearon items are ignored. - Bounds. No recursion (a 50,000-deep nest of flex containers lays out), 100,000 items lay out in well under a second, the measurement count for a 2,000-level alternating nest is at most twice the depth, and relayout of a retained tree allocates nothing (the zero-byte assertion covers a page of more than a thousand flex items).
- Not done:
visibility: collapseon items,list-itemflex containers' outside marker, percentage andmin-contentflex-basisin column containers (treated ascontent), multi-line column containers size trial layouts from the container width rather than the line width, vertical writing modes. Text items take their min-content and max-content from the inline layout's intrinsic sizes and their baselines from its lines.
Fragmentation (Layout/Fragmentation/)
CSS Fragmentation 3 over the fragment tree. The design is lay out as one strip, then cut: a fragmented flow is first laid out
as one tall continuous block at the fragmentainers' inline size, which the block and inline engines already do; the
Fragmenter then finds the break points in that strip, chooses breaks, and rewrites the fragment tree into one fragment per
fragmentainer. Breaking never feeds back into inline layout, because every fragmentainer of a flow has the same inline size.
- Fragmentainers.
FragmentationKindsays what is being filled.Columnis used by multi-column layout.Pageselects the page values of the break properties (page,left,right,recto,versoforce;avoid-pagediscourages) and is what paged media uses withSliceover fragmentainer sizes that differ page by page (Slicetakes the block size of the first fragmentainer, the second and so on, the last one repeating). A paged unit supplies the sizes, callsCollect,Finish,SliceandApply(orPlan), and lays its page boxes and margin boxes around the fragmentainers; the core does not change.left/right/recto/versoare carried inBreakCandidate.Bits; inserting blank pages is the page layer's job. - Strip coordinates are block offsets from the top of the unfragmented flow. A break point is a pair: where the content above ends
(
Ye) and where the content below resumes in the next fragmentainer (Ys). The distance between them is the truncated margin. - Break points (§4.1), found by one explicit-stack walk of the strip (
Collect): class A between in-flow block siblings and class B between the lines of a block container. Class C (between a container's content edge and its content) is used only for the empty room below the last child of a box, which behaves like an empty box. Floats are not break points and are never cut. Boxes that cannot fragment are monolithic (§4.1): replaced boxes, markers, scroll containers (overflowother thanvisible/clip), boxes withcontain: size, and nested multi-column containers. Their insides contribute no break points and a forced break inside them has no effect. On pages a monolithic box or float taller than the page stays whole on the page it starts on and its background and borders carry on in the pages the overflow reaches, as pieces without content. - Flex, grid and tables. A flex container fragments between its flex lines (Flexbox 1 §12) and a grid between its rows; items on one line sit side by side, so a break is offered only where no item spans it, and the strip cut then splits each item that crosses it. Reversed flex containers are walked in block order. Page names are ignored inside flex and grid containers. Table rows break between rows; the places inside a row (its cells' lines, and empty cells) are used only as a last resort, when the row is taller than the fragmentainer, so a row that fits the next page moves whole. An empty box with a height has no break points but may be cut anywhere when it is what does not fit (the "break anywhere" last resort of §4.4, applied to boxes that have nothing in them).
- Break values (§3).
break-beforeandbreak-afteron the first and last in-flow child propagate to the parent chain (§3.1.1), and the values at a break point combine. A forced value (columnin columns, the page values in pages) forces the break and overrides avoid values;break-insideof every ancestor applies (rules 2 and 4);alwaysand thepage-break-*aliases are accepted (§3.4). - Choosing breaks (§4.3, §4.4,
Slice). For each fragmentainer: the first forced break that fits wins; if the rest of the flow fits, it is the last fragmentainer; otherwise the break is the latest candidate that fits (counting the border and padding thatbox-decoration-break: clonerepeats at the break) among those no rule forbids. Candidates carry a level: 0 breaks no rule, 1 breaks only orphans/widows (rule 3), 2 breaks an avoid rule (rules 1, 2, 4). The search takes the latest level-0 candidate, else the latest level-1 one, else the latest candidate of any level, so conflicting avoids end in the latest possible break. If nothing fits, the first break after the point where content stops fitting is used and the content overflows the fragmentainer (monolithic overflow). A break at the very top of a fragmentainer is never chosen unless forced (§4.4 progress), fragmentainers are at least 1px high (§4), and a break that would cut through a float is level 2. Orphans and widows: a break between linesjandn - jis level 1 unlessj >= orphansandn - j >= widows; a block with fewer lines than either value is kept whole. - Margins at breaks (§5.2). At an unforced break the margin between the two boxes is dropped (the next fragmentainer starts at the top of the later box); at a forced break the margin above is dropped and the later box keeps its own top margin (clamped to the collapsed gap). Cloned margins are always zero.
- Splitting boxes (§5.3, §5.4,
Apply). A box that crosses breaks gets one fragment per fragmentainer it occupies. The first keeps the original fragment, so handles stay valid; the others are copies without children, chained after it withNextOfBox. A piece that continues extends to the end of its fragmentainer (the box's content box "fills the remaining extent"). Withslicethe pieces take the original border and padding only at the real start and end (the top and bottom edges of the other pieces are 0, so painting needs no special case); withcloneevery piece keeps all of them and the break leaves less room. Lines, text, floats, absolutely positioned boxes and monolithic boxes are not split; each goes to the fragmentainer where its top is. Every fragment records itsFragmentainerIndex; pieces of a box are in index order. - Bounds. No recursion (explicit stacks, 3,000-deep and 100,000-line flows are tested).
Sliceis O(fragmentainers × log candidates) with prefix tables, so a 100,000-line paragraph cuts in milliseconds. A flow makes at most 100,000 fragmentainers and 4,000,000 extra box pieces; past either the rest stays whole in the last or current fragmentainer and a diagnostic is recorded. All storage is pooled: re-cutting an unchanged flow allocates nothing. - Choices where the specs leave room. Class C points other than the end gap are not used. Floats and absolutely positioned boxes are never fragmented (Fragmentation 3 leaves their fragmentation to the layout mode). A box whose containing block is inside the strip is placed by the strip's coordinates, so an absolutely positioned box is assigned to the column its top lands in. Percentage heights inside a fragmented flow resolve against the container, not the column.
Paged media (Paged/, Layout/PageFragmentation.cs, Tree/ContentList.cs, Tree/GeneratedContent.cs)
CSS Paged Media 3, CSS Generated Content 3 and GCPM 3 on top of the fragmenter. PagedDocument.Create(html, PagedOptions) is the entry; the PDF happy path goes through it.
- Pipeline. The document is cascaded with the
printmedia type, laid out as one strip at the first page's content width, and cut byPageFragmentation.Paginateinto page-sized fragmentainers placed side by side (page i at x = i · stride). Each page then gets a page layer: the page box's background and its margin boxes, laid out as a small HTML document of their own (a probe document per distinct page style) and painted in page coordinates. Content is painted once for all pages (PaintContent); a page is a region of that list plus its layer (GetPage,GetPageLayer). @page(PageRules.cs):size(keywords,landscape/portrait, one or two lengths),marginwith percentages against the page box, the selectors:first,:left,:right,:blankand a page name, combined by specificity like selectors (names beat pseudo-classes by the specificity rule of §7.3, later rules win ties). Page i is a left or right page by its number and the document's direction; a right-to-left document starts on a left page. Blank pages that aleft/right/recto/versobreak inserts count in the numbering.- Named pages.
pageis inherited throughauto. A change of the used name between siblings forces a break (§7.3). Page sizes are per page, so the cut is planned again when a named page has a different height: at most 4 passes. Pages whose content width differs from the one the strip was laid out at keep the strip's width (content is cut or padded) and a diagnostic says so. - Margin boxes. The 16 boxes are sized by the algorithm of §5.3 (
ComputeBoxRects): corners are fixed by the margins; the three boxes of an edge share the edge's length by the resolved-width rules, withautowidths distributed by the content's max-content widths (Distribute), and a box that has no content takes no room. A margin box is a flex column withoverflow: hidden, and thevertical-alignof the box sets itsjustify-content. Their text properties inherit from the root element, not from the page. - Generated content.
GeneratedContentevaluatescontent,counter-reset/-increment/-set(includingreversed(), whose initial value is the sum of the negated increments in its scope, found by a bounded walk),counter(),counters(),attr(), quotes,string-set/string(),target-counter(),target-counters(),target-text()andleader()while the front end builds the styled tree. Counter scopes follow the element tree withdisplay: contentselements transparent. A counter name that isnoneor a CSS-wide keyword makes the value invalid and the generated box empty.counter(page)andcounter(pages)are known only after a cut: when content reads them, or reads atarget-counter()of a later page, the content is built again with the numbers of the previous pass, at most three times; if the numbers do not settle the last ones are used and a diagnostic is recorded.string()takesfirst,start,lastandfirst-exceptover thestring-setevents on each page. - Leaders.
leader()evaluates to a pattern between two private marker characters. Inline layout turns that into a text run that takes no room while a line is broken and is given the room the other pieces of the line leave when the line is placed; it shows whole pattern clusters and the text after it ends at the line end. The glyph run's source text is the pattern. Leaders share the room equally. The pattern is not aligned across lines. - Table headers and footers. A
thead/tfootis reserved on every page a table continues onto (CloneTop/CloneBottomof the candidates) and copied there (FragmentTree.CloneSubtree, traitsRepeated). In 1.0.0 the copies don’t show in the PDF: the header copy is placed above the page’s content area, where it is clipped, and the footer copy is not written. The copies are artifacts in tagged output: they stay inside the table's structure element, so the table is one element across pages. Header and footer groups are not split. - Limits.
MaxPages(default and ceiling 100,000) is checked before every cut: forced breaks and blank pages past it are not taken,Truncatedis set and the rest stays on the last page. Acontentvalue is bounded (16 KiB of source, 64 items, 64 KiB of text once evaluated),@pagerules (1,024) and their declarations (256) by count, blocks by length (64 KiB) and nesting (16), counters by scope depth and number (4,096), string-set events by count, scan budgets for reversed counters by node count. Nothing recurses over the tree. - Not done.
position: running()/element()(the cascade does not keeprunning()), footnotes, leaders aligned across lines,@counter-stylerules and the counter styles thatCounterStyles.cslacks (such asgeorgian), the implicitlist-itemcounter for reversed lists.
Multi-column layout (Layout/Multicol/)
CSS Multi-column Layout 1. A container with column-count or column-width other than auto (ComputedValues.IsMulticol) is a
block container that establishes a block formatting context (§2).
- Used count and width (§3.4,
BlockLayoutEngine.ResolveColumns): with onlycolumn-width,N = max(1, floor((W + gap) / (width + gap))); with both,Nis capped bycolumn-count; with onlycolumn-count,N = count. The used width is(W - (N - 1) * gap) / N.column-gap: normalis1em; percentages resolve against the content width. At most 10,000 columns per row. - Layout. The container's children are laid out as a normal block flow at the column width (the frame's content width is the column
width). When the container closes,
Fragmenter.Planpicks each row's column height and cuts the strip; after the whole layout (and the resolution of absolutely positioned boxes)Applyrewrites the container: its children becomeColumnfragments, each positioned atcontentLeft + k * (columnWidth + gap)(mirrored fordirection: rtl) with its content as children relative to the column. A trial layout that is discarded (flex measurement) drops its plans. - Rows and spanners (§6). A direct in-flow block child with
column-span: allis laid out at the container's full content width and splits the flow into rows: the content before it is balanced on its own, then the spanner, then the content after. The gaps in the strip around a spanner (margins) are kept. A row with no content creates no columns. Spanners deeper than a direct child have no effect yet. - Height. Every row's columns share one height. Non-final rows always balance. The final row balances (
column-fill: balance, the default) or fills (auto): balancing finds the smallest height, in steps of 1/64 px, at which the row fits inNcolumns without overflow and without breaking an avoid or orphans/widows rule (Fragmenter.Balance: a binary search of at most 40 trial slices, repeated once with the rules relaxed if that fails; each trial stops as soon as it needs more thanNcolumns). With a definiteheight(or amax-height) the final row is limited to what is left; if the content still does not fit, the extra columns are added in the inline direction after the last one (§8) and become scrollable overflow. Withcolumn-fill: autoand anautoheight all the content stays in one column unless a forced break splits it. The container's height is itsheight, or the rows and spanners, clamped bymin-height/max-height. - Column rules (§4):
Painterdraws one border item (part 10 of the container's box id, sub id the column's index) in the middle of the gap between two adjacent columns of the same row, as high as the columns, after the container's background and border and before its content. Rules separate columns that exist; a spanner ends the run.column-ruleandcolumnsexpand to their longhands (ShorthandFallbacks). - Intrinsic sizes (no section in level 1): min-content is
N * max(content min, column-width) + (N - 1) * gapwithN = max(1, column-count); max-content is the same with the max-content width. - Scrollable overflow of the container is its border box plus the columns, so overflow columns extend it sideways; overflow of the content inside columns is not propagated past the container.
- Not done:
column-span: allon non-children, nested multi-column containers that break across the outer columns (a nested container stays whole in one outer column), fragmentation of flex, grid and table containers,column-height,column-wrap, and vertical writing modes.
Grid layout (Layout/Grid/)
CSS Grid 2 as a formatting context of the block engine. OpenChild calls GridOpen for a container whose context is grid (flex has the same hook one line above); GridIntrinsicSizes answers min-content and max-content widths for shrink-to-fit and for items that are grids themselves (display: inline-grid is an atomic inline through LayoutIndependent). A grid nested more than 24 grid items deep is laid out as a block with a diagnostic.
- Style.
GridTypes.csholdsGridTrackList(head,repeat()body, tail, line names,subgrid),GridTrackSize,GridLine,GridAreas;StyleComputer.Grid.csparses the longhands andShorthandFallbacks.Grid.csthe shorthands (grid,grid-templatewith the ASCII-art form,grid-area,grid-row,grid-column,place-*). Property ids are appended afterColumnGap. - Contexts. A grid and every subgrid below it form one flat list of contexts built parents first; an axis of a context either owns its tracks or points to the owner context with an offset and a span. Everything is held in per-depth pooled arrays (
GridLevel), so a relayout of an unchanged grid allocates nothing; named lines are the exception (the name index allocates per layout). - Placement (
GridPlacement.cs, §8): line resolution with names, spans, negative lines and implicit names from areas; sparse and dense auto-placement for row and column flow over an occupancy bitmap;ordersorts the items first. Subgrid placement is clamped to the parent span. - Track sizing (
GridTrackSizer.cs, §12): initialize, resolve intrinsic sizes in span order (items spanning flexible tracks last, and only they raise flexible tracks that have an intrinsic minimum), maximize, expand flexible tracks (find the size of an fr; indefinite space uses the items' max-content contributions), stretch auto tracks.auto-fitcollapses unused tracks and their gutters. Columns are sized parents first; rows children first, because a subgrid's height is an input of its parent's rows. Row percentages resolve in a second pass once the container height is known. - Items. Each item is measured by laying it out at its used width with
_absForceand truncating the fragments (FragmentTree.Mark/Truncate), then laid out again at the final size in its area. Replaced items get their used size through_gridReplacedForce. Baseline-aligned items in the same first row get a shim so their baselines meet; a box without a baseline uses its border-box bottom. - Subgrid (§9): subgridded items contribute their own children to the parent's tracks; the subgrid's margin, border and padding at an edge, and half the gap difference elsewhere, become extra margin on the items there. Line names from the parent are visible inside, and a subgrid with an automatic span covers as many tracks as its name list.
- Absolutely positioned children (§10): with grid lines the containing block is the grid area (only when the grid container is the containing block; a missing line is the padding edge); without lines the static position is the box's own alignment inside the container's content box.
- Limits. 10,000 tracks per axis (a 1,000,000-track
repeat()is clamped, line numbers clamp to the same bound as §5.4 allows), 1,048,576 items per container, 50 million cell visits in placement (then the remaining items stack one per row), 100 million units of sizing work, 32 levels of shared subgrid tracks (deeper ones become independent grids). All checks run before the work they guard; nothing recurses on tree depth. - Known deviations. Inline-axis
baselinefalls back to start; last-baseline container baselines are not produced;justify-items: legacycomputes tonormal; masonry (grid-lanes), fragmentation across pages and non-horizontal writing modes are not implemented. - Checked by.
GridLayoutTests(placement, auto-repeat, sizing, alignment, baselines, subgrid, abspos, 0 B relayout of a 1,000-item grid with subgrids, hostile limits), thegridfuzz target,testdata/grid-lab/against Chrome (results/grid/summary.md) and WPTcss-grid.
Tables (Layout/Table/, BoxTree.Table.cs)
- Box generation.
display: table,table-row,table-celland the rest build the table box tree; missing parents and children are generated as anonymous table objects (CSS 2.1 §17.2.1) by an iterative pass over the box tree, so nesting depth never reaches the call stack. HTML tables get their display values from the user agent sheet;colspan,rowspanandspanare read by the HTML front end and clamped to 1000 and 65534 (HTML §4.9.11). - Grid. Rows and cells are placed into a slot grid (spans,
thead/tfootordering,col/colgroup,visibility: collapserows and columns). Limits: nesting 16 tables deep, 1000 columns, 4 million collapsed-border slots; a table nested deeper is laid out as blocks, cells beyond the column limit are dropped, and each case records aLimitExceededdiagnostic. - Columns. Auto layout takes each column's min- and max-content width from its single-column cells, then distributes the excess of spanning cells by max-content weight, preferring columns that are not fixed. A table narrower than its columns' sum in the min case overflows; wider space goes first to auto columns, then fixed, then percent. Percent columns are served first when the available width allows.
table-layout: fixedtakes widths from the first row andcolelements only (CSS 2.1 §17.5.2.1). A caption's min- and max-content widths widen the table. - Rows. A row is as tall as its tallest cell (spanning cells share the excess among the rows they span) and at least its specified height.
vertical-alignin a cell is top, middle, bottom or baseline (§17.5.3); baseline cells align on the first baseline in the row. A cell is laid out once; percent heights inside cells do not resolve. - Borders. With
border-collapse: separate,border-spacingand the cell borders are ordinary box geometry andempty-cells: hidesuppresses empty cells' decoration. Withcollapse, borders resolve per edge by the conflict rules of §17.6.2.1 (hidden wins, then width, then style, then cell over row over group over column over table); the before half of a shared border isfloor(width/2)and the after half is the rest. The winners are stored per fragment (FragmentTree.TableBorders) and the painter draws them in place of the box's own border. - Captions. A caption is laid out above or below the rows by
caption-side. It lives inside the table fragment (the table wrapper and the table box are one fragment), so a table border and background also surround the caption; this differs from the specification, where the wrapper is the table's parent. - Backgrounds. Table, column group, column, row group, row and cell backgrounds paint in that order (§17.5.1). Column and column group fragments exist only when they have a background.
- Presentational hints (HTML §15.3.8).
width,height,cellspacing,cellpadding,borderandalignontable,width,heightandalignontdandth, andalignontr, apply where the computed value is still the user agent default, because the cascade has no hint layer.alignon a cell centers a table child the way a centered block does in a browser.bgcolorandvalignare not implemented. - Fragmentation. In paged media the rows of a table are break points and a row's cells are never split; header and footer groups are reserved on each page the table continues onto (see Paged media above).
- Cost. A relayout of an unchanged table reuses the grid and allocates nothing (checked for 1,000 cells); a 10,000-row table lays out in time and memory linear in its rows.
- Checked by.
TableLayoutTests(anonymous objects, spans, auto and fixed layout, collapsed borders, captions, alignment, hints, 0 B relayout, the 10,000-row shape, hostile spans and nesting, relayout equal to a fresh layout), thetablefuzz target, WPTcss2-tablesandcss-tables, and thetemplatesset against Chrome.
Paint (Painting/Painter*.cs)
The painter walks the fragment tree in CSS 2 Appendix E order and writes the display list (display-list.md). The second paint area added the pieces below; each is computed from typed values in Style/PaintTypes.cs and painted by one method.
Backgrounds (Backgrounds 3 §3). One painted layer per
background-imageentry; the other lists cycle (§3.1). Layers paint last to first underbackground-color, which fills the clip box of the final layer. Origin, clip (border-box,padding-box,content-box; rounded by the box's radii) and size (autowith the natural ratio,cover,contain, lengths) follow §3.4 to §3.9. Repeat follows §3.5:repeat,no-repeat,space(the whole number of tiles that fit, spread with equal gaps; a single tile is placed at the start) andround(the tile is rescaled to fit a whole number, at least one).background-position-x/-yare the longhands (OneHtml 1.0 expands the shorthand into them). Aurl()layer that repeats is one tiled image item. A gradient layer is one rectangle per tile on snapped edges, so no seam shows.fixedattachment is relative to the viewport: the layer sits inside a translation thatPatchScrollupdates in place, the same entry the fixed-position patch uses. The canvas takes the root's (or the body's) background (§2.11.2).background-clip: textandborder-areaare not painted and record a diagnostic.Gradients (Images 3 §3). Linear, radial and conic, plain and repeating. Stops get the fixup of §3.4.1 (missing positions spread evenly, positions raised to the largest one before, transition hints expanded to a few intermediate stops). Interpolation is premultiplied sRGB, which is what the display list defines and what Chrome does for legacy colors; the
in <colorspace>syntax is not parsed yet.Borders. Widths floor to whole device pixels (minimum one).
border-image(§6) slices the source into nine parts, paints the corners, repeats, rounds, stretches or spaces the edges, and draws a gradient source through a transform and clip;fillpaints the middle. Radius clipping of backgrounds is by clip box (§5.3).Shadows (§7). Box shadows paint last to first, outer before the box, inset after the background and before the border, with spread and blur.
Outlines (UI 4 §4). Drawn after the content of the paint unit that owns them, outside the border edge, offset by
outline-offset, with the border-style look.outline-style: autois a 2 px ring in the outline color with a 1 px white ring inside it, a fixed deterministic look; it follows the border radius.Text paint.
text-shadowpaints each shadow as a blurred copy of the glyph run before the text, the first shadow on top. Decorations (Text Decoration 4) are drawn by the box that declares them across its inline text: underline and overline under the text, line-through over it. Thickness is the font'spostunderline thickness (minimum one device pixel) and the line's edges snap like any rectangle. The underline's center sits half the font's descent below the baseline, which is what Chrome 156 does with the packaged fonts rather than what theposttable's underline position says: probing 10 to 48 px Liberation Sans, the rule puts the underline on the same rows as Chrome at 10 of 13 sizes and one row off at 18, 24 and 40 px (UNDERLINE=1mode oftools/snap/probe.py); other families are not probed. The strike-through comes from theOS/2strikeout metrics;text-underline-offsetand-position: underoverride;solid,double,dotted,dashedand a zigzagwavy. Skip-ink is off. Markers:disc,circleandsquareare shapes beside the first line;decimal, alpha, roman and greek markers are text built from the item's index among its list siblings;list-style-imagedraws the image.start,reversedandvalueare not read, andlist-style-position: insidemarkers are not painted.Replaced content.
imgpaints throughobject-fit,object-positionandimage-rendering(Images 3 §5); the natural size comes from the host'sIImageSource. A missing image paints nothing and records a diagnostic; alt text and the broken-image icon are not drawn.Pixel snapping. Every edge of a background, border, outline, clip and decoration rectangle is placed at
round(x · dpr) / dpr(half up), and the far edge is snapped on its own. Evidence (Chrome for Testing 156.0.8078.47, headless,tools/snap/probe.py; columns shown as.white,Rred,Bblue):Case Chrome result left 10.4, width 10.4 red on columns 10 to 20 (edges 10 and 21) left 10.5, width 10 columns 11 to 20 (10.5 rounds up to 11, 20.5 to 21) left 10.6, width 10 columns 11 to 20 boxes at 0 and 10.5, both 10.5 wide red 0 to 10, blue 11 to 20, no blended column boxes at 0 and 10.3, both 10.3 wide red 0 to 9, blue 10 to 20 scale 2, left 10.25, width 10 20 device pixels from device column 21 (20.5 rounds up) scale 2, left 10.2, width 10 20 device pixels from column 20 scale 1.5, left 10.2, width 10 15 device pixels from column 15 top 10.5, height 10 rows 11 to 20 border 0.4, 0.9, 1.0, 1.6 px at scale 1 1 px border 2.5, 2.99 px at scale 1 2 px border 0.4, 0.6, 1.3, 1.6 px at scale 2 1, 1, 2, 3 device pixels Known difference: a box narrower than half a device pixel whose edges snap to the same pixel is painted one pixel wide by Chrome and not at all by us.
Resources. The painter never loads anything: images come from
PaintOptions.Images, fonts fromPaintOptions.Fonts..ImagesuppliesImageStore, which decodes through oneimage from the host'sResourceslookup and answers all of layout (IResourceResolver), painting (IImageSource) and rasterizing (IImageResolver).Ids. Items of this area take ids from a second id space (
Id2, keyed by the owner, a part number and an index) with a free-id search on collision, so ids stay stable across unchanged frames.Allocation. Repainting an unchanged page with layered backgrounds, border-image, shadows, outlines, decorations, text-shadow, markers and images, and patching fixed backgrounds on scroll, allocate 0 bytes once warm (
PaintAllocationTests).Checked by.
PaintStyleTests(parsing and computed values),PaintPixelTests(analytic pixels plus two golden scenes),PaintAllocationTests.
Third paint area: transforms, effects and inline SVG (Painter.Effects.cs, TransformGeometry.cs)
- Transforms (Transforms 1 §6 to §13, Transforms 2).
transform, the individualtranslate,rotateandscale,transform-origin(with z),transform-box,perspective,perspective-origin,transform-styleandbackface-visibilityare typed values inStyle/EffectTypes.cs.TransformGeometrycomposestranslate · rotate · scale · transformaround the origin into one 4×4 matrix in the containing space; the painter and hit testing both use it, so they agree. A parent'sperspectivemultiplies the matrix of each child. A box whose back faces the viewer withbackface-visibility: hiddengets an empty clip.preserve-3dis flattened (a diagnostic when a projection is used); nested projections are flattened to their affine part. A matrix that does not map the plane in front of the viewer paints nothing. - Layout effects of a transform (Transforms 1 §2). A box with a transform or
perspectiveis the containing block of its absolute and fixed descendants:Frame.CbFragstops at it, andFrame.TfFragis the nearest such ancestor forposition: fixed(which then scrolls with it and no longer takes the viewport patch). The bounding box of the transformed border box and transformed overflow replaces the untransformed extent in the parent's scrollable overflow (BlockLayoutEngine.Transform.cs; block, flex and grid items; floats and table cells are not handled yet). - Scopes. One element opens, outermost first: transform (and backdrop filter, a leaf before the group), group (opacity,
mix-blend-mode,isolation), mask, clip-path, filter, then the element's own painting;ExitFxandExitTransformclose them in reverse. All of them count against the 256-level scope budget and stop opening before it with aLimitExceededdiagnostic. - Filters (Filter Effects 1). The
filterfunctions resolve to at most 32FilterOps (blur and drop-shadow radii capped at 4096 px).url(#id)filter references are not rendered (diagnostic). - Clip paths (Masking 1 §5).
inset()with rounding,circle(),ellipse(),polygon()(at most 4096 points),path()(SVG path data throughPathDataParser, arcs become cubics, at most 8192 segments) and the geometry boxes (border-box,padding-box,content-box,margin-box; the SVG boxes act as the border box).clip-path: url(#id)names a<clipPath>of an inline svg in the same document: the front end flattens itsrect,circle,ellipse,line,polyline,polygonandpathchildren to one path (SvgClipDefs, at most 256 per document), in user space or, forclipPathUnits="objectBoundingBox", scaled to the border box. Atransformon a clip child,useinside aclipPathand aclip-pathon theclipPathitself are not applied. - Masks.
mask-imagelayers (gradients and images) with the mask longhands and themaskshorthand, at most 16 layers, are painted through the background layer code intoPushMaskLayerscopes.mask-image: url(#id)to an inline<mask>is not rendered (that layer masks everything, with a diagnostic). - Compositing.
mix-blend-mode,isolationandbackground-blend-mode(a group around the background layers of the box). - Inline SVG. An
<svg>element is replaced content. Its natural size is itswidthandheightor the ratio of itsviewBox;HtmlFrontEndserializes the subtree (iteratively, at most 20,000 nodes and 1,048,576 characters) and the painter emits oneSvgImageitem;.Imagerenders it through OneImage's SVG renderer at the item's size. Text inside an inline svg is not drawn yet (the SVG renderer is not given the engine's fonts), and the page's CSS rules do not reach inside the svg. - Allocation. Repainting a page with all of these allocates 0 bytes once warm, and patching a transform or an opacity needs neither layout nor allocation (
EffectsPixelTests). - Checked by.
EffectsPixelTests(analytic pixels per family, a golden scene, allocation),DisplayListEffectsTests(limits, nesting, patching, damage), theeffectsfuzz target, WPT css-transforms, css-masking, filter-effects and compositing.
Interaction (Interaction/)
InteractivePage wraps an HtmlPage with everything a host needs to feed input: hit testing, DOM events, focus, hover/active state, scrolling input and CSSOM View queries. Inputs are plain structs (PointerInput, WheelInput, KeyInput) carrying the host's timestamp; the engine has no clock, so double-click timing uses those timestamps. A host that draws the display list with its own GPU renderer finds the whole surface in docs/game-hosts.md: host scopes (display list 1.3, the --host-scope property), per-handle image invalidation, stable glyph-face and SVG keys, name lookup (FindElement, StyledTree.FindByName) and a synthetic Click.
- Hit testing (
HitTester). The fragment tree is indexed once per page into stacking contexts and the CSS 2.1 Appendix E phases, as the painter walks them, then searched in reverse paint order with an explicit stack (no recursion on tree depth). A candidate is rejected when it is outside an ancestor clip (overflow, rounded corners),visibilityis not visible, orpointer-events: none. Scroll offsets and transforms of the ancestors are applied to the point (at mostMaxEffectChain= 1024 such ancestors per fragment; beyond that the outermost are ignored). A transform makes a stacking context. The root element receives hits on the empty canvas; points outside the viewport hit nothing. A hit on text carries the character offset under the point. Known limits: sticky offsets,clip-path, masks and filters are not applied. A point within about 0.02 px of a transformed box's edge may differ from Chrome. - Events (
EventDispatcher). The DOM Living Standard dispatch algorithm: the path from the target up through the document to the window; capture, target and bubble phases;stopPropagation,stopImmediatePropagation,preventDefaultwithcancelableandpassive;once; listener lists snapshotted per node so additions and removals during dispatch behave as specified;composedPath().EventTargetis a handle over a OneHtml node or the window; C# listeners and a later script runtime use the same registry. The path is capped atMaxPathLength= 2048 targets (document and window always kept). Event objects are pooled and valid only during the listener call. - Pointer, focus and keyboard order (UI Events). Each pointer event is followed at once by its mouse compatibility event. Moving between elements fires
pointerout,mouseout,pointerleave,mouseleave,pointerover,mouseover,pointerenter,mouseenter(leave/enter on each ancestor that changes), thenpointermove/mousemove. A canceledpointerdownsuppresses the mouse events until release but not focus.clickgoes to the nearest common ancestor of the down and up targets; the click count grows while presses are within 500 ms and 5 px. Focus changes fireblur,focusout,focus,focusin. - Focus (
FocusRules). Focusable areas and the sequential order of HTML §6.6: positivetabindexfirst in ascending order (ties in tree order), thentabindex=0and natively focusable elements in tree order; negativetabindexis focusable but not a tab stop. Tab wraps by default.:focus-visiblefollows the Selectors 4 heuristic: keyboard-caused focus matches, pointer focus on a non-text control does not.:hover,:active,:focusand:focus-withinare applied by restyling with a newStyleContext. - Scrolling input. Wheel (line = 40 px, page = 0.875 of the viewport, Shift turns vertical into horizontal) and keys (arrows, Page Up/Down, Home/End, space) go to the nearest scrollable ancestor and chain to its parent at the limit, ending at the viewport. They use the scroll patch of the Overflow section, so there is no relayout, and fire
scrollevents.ScrollIntoViewscrolls the nearest way. - CSSOM View (
LayoutQueries).GetBoundingClientRectandGetClientRects(after scroll and transforms) and the layout-spaceOffset*,Client*,Scroll*metrics.offsetLeft/offsetTopare relative to the offset parent's padding edge, or to the page origin when the offset parent is the body or null.scrollWidth/scrollHeightare the padding-edge-based overflow extent, never belowclientWidth/clientHeight. Values were compared with Chrome 156 on three probe pages (testdata/interaction, produced bytools/interaction/probe.sh). - State-change seam. After an input that changes
:hover,:activeor:focusstate, the page computes the list ofElementStateChange(the element and theElementStatesbits that flipped, for every element in the affected chains) and passes it toStateChangeHandler(IStateChangeHandler.ElementStatesChanged(page, changes, context)) with the newStyleContext. If the handler returns true it has restyled and relaid out itself; otherwise the page restyles from the new context and rebuilds the box, fragment and hit structures when any computed value changed.HandlePointer,HandleWheelandHandleKeyreturnInputOutcomeflags. An incremental restyle engine plugs in at this interface. - Allocation. A hit test, a pointer move that changes no hovered element, event dispatch without state change, wheel scrolling that keeps the hovered element, and viewport scrolling allocate 0 bytes once warm (
InteractionAllocationTests). A change of hover or focus state on an HTML page restyles in place (see Incremental updates); OneHtml's cascade still allocates about 24 KB for a hover move on the 1,000-node sample, while the same change through the styled-node tree allocates nothing.
Forms (Forms/, HtmlFrontEnd.Forms.cs, Painter.Controls.cs, InteractivePage.Forms.cs)
Controls are ordinary boxes with Chrome's default metrics; docs/forms.md holds every rule and its probe evidence.
- Style. When the styled tree is built (and again on restyle),
AdjustControlStyleclassifies the element (ControlKind), writes the widget size intoautowidth and height, makes the text single-line where needed and records paint-only values (Control,ControlState,ControlFraction; they differ in paint, never in layout).WidthIsIntrinsic/HeightIsIntrinsiclet stretching and both-inset positioning treat the widget size asauto. Adisplay: grid | flexfieldset is split into a block and an anonymous content box that carries the grid or flex properties. - Layout. Block layout centers the lines of buttons, text fields and drop-downs in a taller box (
CentersItsContent, skipped on a memo hit because the copied children are already moved); intrinsic sizes of a control are its widget width. - Paint.
Painter.Controls.csdraws the widgets from the control state and the fragment box (accent color, focus ring, disabled colors); withappearance: nonethe normal box painting applies. - State.
FormStateholds what the user changed (checked, selected options, range values, open select);HtmlPage.Formowns it,RefreshContentrebuilds the value and option text. On the builder pathStyledTree.SetControlstamps the kind and state per node, and the incremental pipeline treats it as a style change (0 B once warm). - Interaction. Activation, labels, radio groups, submit and reset with constraint validation, range drag and keys, and the select popup go through the normal DOM event dispatch (
input,change,submit,invalid,reset); the popup is a synthetic box painted in a top layer above the root stacking context. Text editing (Forms/TextEditor,TextEditSession,TextGeometry,InteractivePage.Editing.cs,LiveTextEditor,Painter.Editing.cs) is specified indocs/forms.md; the painter reads aTextEditViewfor caret, selection and preedit.
Incremental updates (LivePage, PipelineUpdater, StyledTree.Changes.cs, BoxTree.Incremental.cs, BlockLayoutEngine.Reuse.cs)
One change-set model serves both front ends. The result of any sequence of edits followed by a flush equals building the page from scratch from the final tree; the equality fuzz (IncrementalEqualityTests, the incremental fuzz target) checks styles, boxes, fragments and the display list with item ids after every edit.
- Change set.
StyledTreelogs each edited node once, with flags (Style,Text,TextStructure,Children,Replaced,Added,Removed), coalesced per node. The log holdsMaxPendingChanges(4,096) nodes; beyond thatPendingOverflowedis set and the next flush rebuilds everything, which is also the fallback for a huge edit. Edits:SetDeclaration,SetProperty,SetStateStyle(hover, active and focus layers, applied focus < hover < active),SetStates,SetText,InsertElement,InsertText,Remove,Move,Invalidate. A removed node keeps its index and handle until the flush has consumed the change set; then its slot is reused (see Long-running stability). HTML pages feed the same machinery fromIStateChangeHandlerstate changes (HtmlPage.TryRestyle). - Restyle. Dirty nodes are recomputed; a child is re-evaluated only while its parent's style id changed, so inheritance stops where computed values do not change. Styles are interned in
StyleTable; an unchanged result keeps the same id. A viewport change resets the table and recomputes everything. - Boxes. A style change that leaves
display, out-of-flow-ness,white-space-collapseandlist-style-positionunchanged is patched into the existing boxes (anonymous boxes below follow their parent's style) and the box and its ancestors are marked dirty. A structural edit (a node inserted, removed or moved, text that becomes empty or white space or stops being, a style change todisplay, out-of-flow-ness,white-space-collapseorlist-style-position) patches the box tree in place (BoxTree.Structure.cs). The nearest ancestor of the edit whose box is a block container has its children generated again from the styled children, by the same code a full build runs: inline boxes and generated-content boxes on the way are made again with everything below them, the container's anonymous boxes (CSS2 §9.2.1.1 block wrappers, §17.2.1 table fix-up, flex and grid item wrappers) are dissolved and made again, blockification and white-space trimming are decided over the whole child list, and an inline box split around a block is made again with its continuations. A direct child whose node did not change keeps its box and everything below it, so layout reuse (box dirty marks) and the paint journal see it as the same box. Boxes that go away return their slots to a free list that the next new box takes, so a UI that adds and removes nodes every frame keeps its columns the same size; new boxes and the container's chain are marked dirty and hazards are recomputed for the new boxes and the ancestors. Box numbers are therefore not in tree order after an edit; nothing reads them as an order (the percentage-height flag of flex memoization is computed over the tree, not over numbers), and display-list ids, which name a box by its number, stay the same for every box an edit did not remake. The equality tests compare ids by each box's place in the tree. Only these still generate every box again: a change to the root element's owndisplay, out-of-flow-ness,white-space-collapseorlist-style-position; more thanMaxPendingChangesedited nodes in one flush; a viewport change or a font change (every style row is recomputed); a cancelled flush; the box limit reached while generating. The epoch (which drops every layout record) only moves on a full build. The painter paints the whole list after a structural edit (the fragment tree no longer has the old shape); a display-list patch is for edits that keep the shape. When the old and new styles differ only in values that paint reads (colors, backgrounds, shadows, outlines, text decoration, opacity) no layout runs. Transforms, filters, masks andclip-pathtake the layout path because they can make containing blocks or ink overflow. - Layout reuse. Layout writes into a scratch fragment tree and swaps it into the published one, so handles stay valid. When a block, flow-root, flex or (non-subgrid) grid container that is not dirty and whose subtree has no floats, out-of-flow boxes, columns or tables is opened with the same frame as last time (width, content origin, bottom edge, content width, specified, minimum, maximum and ratio heights, masked flags, grid, table and nesting depth), its children's fragments are copied from the earlier tree and the values the children leave in the frame are restored (a flex container also restores the definite heights it sets while closing). There is one record per box, stamped with the layout that wrote it: a record written in the current layout is never read back, dirty boxes' records are dropped when a layout starts, and a visit that cannot be described by a record drops the box's record. The copy moves a contiguous (preorder) run of fragments column by column, shifting the child and sibling links by a constant with vector adds, and falls back to one fragment at a time when the run is not contiguous. Flex item measurements (the trial layouts) are kept between layouts for boxes whose subtree did not change; a box that was dirty in a layout loses its earlier measurements for good, so they cannot come back when it is clean again. Intrinsic-size caches are kept and cleared along dirty chains. A new box-tree epoch, font catalog version or output object invalidates everything. A trial layout that is undone (flex measurement, grid sizing) drops the records and column plans made inside it. The layout also leaves a
LayoutDelta: whether the new fragment tree has the same shape as the published one (same count, kinds, boxes and parents) and which fragments it wrote anew with a different position, size, text run or overflow. - Paint. The retained display list is repainted with stable ids; damage is computed from item content and context hashes (display list format 1.2). Transform and opacity patches are unchanged. After a full paint the painter keeps a journal: for each fragment the span of items it wrote, its paint-order position (phase list and z-index) and flags for what makes it unsafe to rewrite alone (scopes opened around it, clips of containing blocks, text decoration, outlines, tables, multi-column, sticky, effects). When the layout delta is aligned,
Painter.TryPatchre-classifies only the fragments in the delta (and the subtrees below the ones that moved) and the boxes whose style or text was patched (BoxTreerecords them; text boxes below a restyled box are included), then paints just those over their old slots (DisplayList.BeginRewrite: items are built at the tail, moved into the old slot, payload of equal size is overwritten in place, damage collects the old and new bounds). It gives up and the whole list is painted when a fragment's paint-order position, owner or flags changed, an unsafe fragment is touched, the item sequence would differ, the payload grew past twice its size at the last full paint, the viewport scroll range changed whether the page scrolls, or the changed set exceeds half the fragments. The canvas item is rewritten when the root's extent changed.InteractivePage.Repaintdoes the same for pages built from HTML, and patches transform and opacity animations of elements that did not change otherwise (the animation controller reports them apart from paint and layout changes, so they are neither marked dirty nor relaid out). - Flush and queries.
LivePage.Flushruns restyle, box patching, layout and paint and returns what ran.LayoutQueriesover aLivePageflushes pending edits before answering (CSSOM View's forced layout); over anHtmlPagethe changes are already applied. The hit tester re-indexes when the fragment tree's version moves, not only when its count does. - Long-running stability. A UI that adds and removes nodes every frame for hours must not grow, so nothing here is append-only.
- Slots.
Remove(and the marker removal a style change can cause) queues the root of the removed subtree. When the flush has consumed the change set (StyledTree.ClearPending), every node of every queued subtree gives its slot back: its columns are reset (declarations, replaced source, states, control info, text span), its generation moves on and its index goes on a free list; the nextNewNodepops it. A tree no page tracks reclaims when an insert finds no free slot.Countis the number of slots ever needed at once (an index bound, not a node count);LiveCount,FreeSlotsandCapacitysay the rest.MaxNodesbounds the nodes in the tree, not the slots ever used. Every column indexed by node (_chg,_oldStyle, the declaration layers, ...) is sized by the slot count, so the arrays stop growing once the peak is reached. The box tree's node-to-box table is kept up to date by the structural patch (a freed box clears its node's entry; the tables grow with the slot count), so nothing keyed by node index outlives a flush. Per-node state that lives outside the tree (event listeners, hover, focus and animation state) is keyed by OneHtml node on pages built from HTML, whose document is immutable, so it never sees a slot reused; builder pages have none. AScrollStateis keyed by the scroll container's element (styled node index plus the slot's generation), so an offset stays with its element through inserts, removals and moves around it, and is forgotten when the node is removed (its slot's generation moves on, so a node that takes the slot starts at 0). - Handles.
StyledNodecarries the slot's generation next to the index.IsValidis false once the node's slot was reclaimed or the tree cleared. Editing methods reject an invalid handle withArgumentException; reading members throwInvalidOperationException;BoxTree.PrincipalBox,LayoutQueriesandComputedStyleSerializeranswer "no box" or null for one. Equality includes the generation, so a stale handle never equals the node that reuses its slot. Until the flush after aRemovethe removed handles stay valid, because the change set still names them. - Text. The text arena compacts when it fills (including while texts are added), counting only live nodes, so it follows the live text and not the text ever stored; the text limit counts live text.
- Style caches. Styles computed from a builder
StyleDeclaration(and from the layer keys of hover, active and focus declarations) are remembered on the declaration through aConditionalWeakTable, at most 32 entries per declaration, not in the table's global map. A host that makes a declaration per inserted node does not keep them alive; one that keeps and shares declarations keeps their entries, so reusing them computes nothing. The OneHtml front end keeps its map (its keys live as long as the document). - Style rows.
StyleTableinterns distinct computed styles by value (at most 65,536 rows at a time), so no row has one owner that could free it; the table sweeps instead (StyleTable.Reclaim, mark and sweep). Every object that keepsStyleIds registers with the table as anIStyleHolder(weak reference):StyledTree(the style of every live node, and the old style of the nodes with a pending change),BoxTree(every box, anonymous boxes included) and theAnimationController(the private rows of animated elements and of the descendants that follow them). A sweep clears a mark per row, asks each holder to mark what it still reads, then frees every unmarked row to a free list (lowest index first, and the high-water mark shrinks when the top rows are free). Per-row bookkeeping follows: the by-value index is rebuilt, entries of the strong caches (_intern, anonymous-box rows) that name a freed row as value or parent are removed, and a per-row generation moves on so entries of the weak per-declaration caches that name a freed row stop matching (checked at lookup, so the weak tables are never walked). The inline layout's font-metrics cache is keyed by row and drops its entries throughStyleTable.ResetGeneration, which a sweep that freed rows also bumps.- When.
PipelineUpdater.UpdateBoxesAndLayoutcallsReclaimIfNeededafter it cleared the pending changes: the one point of a flush where no style id is held in a local, forLivePageand the HTML page alike. A sweep is due when the rows in use exceed the number the last sweep left by at least 1,024 (more for a big document: the larger of 1,024 and the rows the holders keep, up to 16,384), so a sweep costs a few microseconds per thousand frames of the HUD page and allocates nothing.Reclaim()is also public for a host that wants to sweep at a time of its own, with the same rule: noStyleId(fromStyledNode.Style,Box.Style,GetAnonymous) may be kept across a flush. - One interval of grace. A row that no holder reads survives a sweep when it was looked up or made since the previous sweep (a per-row epoch stamp). A style that a host asks for again and again through a cached declaration (the variants of a damage-number row) is never computed twice, because its row is touched in every interval; a style that was used once and dropped (a position that took a new value) goes at the second sweep. The number of rows therefore settles at what the holders keep plus about two intervals of new values.
- Releasing private rows (
ReleasePrivate) keeps the values and moves the row's generation on; the row can be handed out again at once. The transition code still reads a released row as a node's "before" style until its restyle is done, so rows are never cleared. - Proven by sweeping after every flush in the edit-sequence tests whose edits mostly make values no row holds (
EditSessionfresh-values profile; incremental equals a fresh build, and theincrementalfuzz target has the same profile), and by the soak below.
- When.
- Measured. The HUD page with a moving element (its
leftis the frame number in px and a label'swidthcycles) on top of the churn, a new style row per frame: the table size, live rows and heap stay flat over 1,000,000 frames and the engine's share of a frame (the flush, leaving out the host's own work of parsing the new values) allocates 0 bytes in steady state (ChurnSoakTests;ONERENDER_SOAK_FRAMES_MOVINGsets the length, default 30,000). At 1,000,000 frames in a release build: style rows 3,304 (2,347 live) after the first 100,000 frames and unchanged to the end, the heap 27,172 KiB after warm-up and 27,139 KiB at the end, 980 sweeps, and 125,048 bytes allocated by all the flushes together (a one-time cost, the same at 28,000 frames). Known limit: computing a style that declaresdisplay,colororbackgroundallocates (OneHtml's typed value parsers), so an element with such declarations whose parent's style changes every frame allocates ~100 bytes per such property on that frame; the soak's moving elements use geometry only. - Measured, churn. The game-UI page (about 1,070 nodes) with a floating layer of about 50 nodes added and the previous one removed every frame, plus a chat line in and the oldest out, then a flush: slot count, capacity, style rows and the heap after a forced collection are identical after warm-up and after the run (
ChurnSoakTests, with a declaration made per node as well as with shared ones), and a steady-state frame allocates 0 bytes with shared declarations.
- Slots.
- Bounds and cancellation. The pending log, node count and depth are bounded; a cancelled flush leaves the edits pending and the next flush starts again from the styled tree.
- Allocation and time. On the 1,000-node game-UI sample (
testdata/game-ui/hud.html, the same scene through the builder), hover restyle, a text change and a one-subtree relayout allocate 0 bytes once warm and take about 3 µs (hover), 50 µs (text) and 0.35 to 0.45 ms (one subtree, which moves most of the page and paints it in full) in a release build on a loaded 32-core server part (IncrementalBenchmarksfor the gated run;GameUiIncrementalTests.WorkPerChangechecks without a clock that none of the changes generates the boxes again). A nothing-in-common reuse is guarded by key tests (ReuseKeyTests); nothing inside a multi-column container is reused, because the column cut rewrites its fragments after the layout.
Animations (Animation/)
CSS Transitions 1 and CSS Animations 1, timed by the host. The engine reads no clock: the host passes the time of the frame to InteractivePage.Advance(nowMs) (any monotonic source, the same one that stamps its inputs; the start is InteractionOptions.AnimationStartTime). HtmlPage.Create without HtmlPageOptions.AnimationTime is a static page: the animated properties show their cascaded values, so documents and reftests are unchanged. docs/animations.md lists the matching choices and deviations.
- Where it sits.
AnimationController.Recalcruns on the styled tree after the front end has built it and before the box tree is made. It compares each element with the same element of the previous tree (the before-change style), starts, retargets, reverses and cancels transitions (Transitions 1 §3), matchesanimation-nameagainst the@keyframescollected from the document's sheets (KeyframesRegistry; the cascade does not carry them), and gives every animated element a private style row that replaces its style in the tree.Tick(now)rewrites those rows in place and reports the most expensive kind of change it made (MotionLevel): none, compositor (onlytransform, the individual transform properties oropacity), paint (colors, shadows, filters) or layout. - Values.
PropertyAnimatorhas one animation type per property: numbers and integers, lengths and percentages (a px and % mix becomes acalc()), colors (premultiplied sRGB), border radii, positions, shadow lists, filter lists,clip-pathshapes of the same kind, transform lists (matching functions per argument, otherwise through 4×4 decomposition inTransformMathwith quaternion interpolation),visibility,line-height,vertical-alignanddisplay; everything else is discrete and flips at the midpoint (Web Animations 1 §5.3), or runs only withtransition-behavior: allow-discrete.EasingFunctionis CSS Easing 1:cubic-bezier()with tangent extrapolation,steps()with the jump terms,linear()with stop filling.EffectTiming.Sampleis the Web Animations timing model (phase, iteration, direction, fill). - Through the incremental pipeline. On an interactive page a state change restyles in place (see Incremental updates);
AnimationController.RecalcPendingthen runs for the restyled elements only, with each one's before-change style taken from the change log, so hover and focus transitions start without a full recalculation. When any restyled element has or had effects, its style row differs in content from the logged old style (rows are rewritten under their ids), so the boxes are generated again before layout; the cascade and the walk over unaffected elements are still skipped. A tick that changes a paint-only or layout property marks only the animated elements and the descendants that follow an inherited value (the controller'sChangedNodes):BoxTree.ApplyAnimatedRowsrefreshes the anonymous boxes below them and, for layout properties, marks their boxes dirty, andPipelineUpdater.LayoutAlllays out only the dirty subtrees and ancestors with the page's reusing layout engine. Rows of finished effects and of replaced followers go back to the style table for reuse. On the HUD page of about 1,800 nodes, ticking width and color on a handful of elements and repainting allocates 0 B once warm (MotionGameUiTests), and ticks equal a fresh render at the same time (HtmlPage.CreatewithHtmlPageOptions.AnimationTime, which starts the animations at the clock's origin and samples them at that time). - Compositor path. An element whose transform or opacity is animated always gets a transform scope or opacity group in the display list, recorded as a patch entry.
InteractivePage.AdvancereturnsInputOutcome.Animatedalone for such a tick;PatchAnimations(painter, list)then replaces the matrices and group opacities in place and adds the damage. No relayout, no allocation (MotionAllocationTests: 100 animations on a page of about 1,500 nodes, 0 B per frame, including frames that deliveranimationiterationevents). If a transform moves between planar and perspective the list cannot be patched andPatchAnimationsreturns -1: paint again. - Inheritance. The text and the descendants that inherit an animated inherited property (
color,visibility,font-size, ...) get private rows too and receive its animated value at each tick. Descendants that set the property themselves, or run effects of their own, keep their values. Lengths inemof element descendants are not recomputed whenfont-sizeanimates. - Events.
transitionrun,transitionstart,transitionend,transitioncancelandanimationstart,animationiteration,animationend,animationcancelare queued by the tick, sorted by the time they were scheduled for, then composite order, kind and element (a total order), and dispatched throughInteractivePage.Events(TransitionEvent,AnimationEvent; they bubble and cannot be cancelled). Events of the first frame wait for the firstAdvance, so listeners added afterCreatesee them. - Reduced motion.
HtmlPageOptions.PrefersReducedMotionsets theprefers-reduced-motionmedia feature; the page's own rules decide what to do. - Bounds. 64 items per comma list, 1,024 keyframe blocks per rule, 100,000 animated elements, 256 transitions and 64 animations per element, 65,536 queued events, 4,096 followers per element; times are clamped to ±1e12 ms and iteration counts to 1e9; easing outputs to ±1e9;
steps()to 2^20 steps andlinear()to its stop limit. Nothing recurses over tree depth. - Out of scope. Scroll-driven animations (
animation-timeline,animation-range),animation-composition,@starting-style, view transitions, registered-property animation, the Web Animations script API,var()inside@keyframes, pseudo-element animations. - Checked by.
MotionEasingTimingTests,MotionInterpolationTests,MotionPageTests(start, reverse and cancel rules, event order, pause, inheritance),MotionAllocationTests, themotionfuzz target, and the replay of the css-transitions and css-animations testharness helpers (WptMotionReplayTests,testdata/wpt-motion): the parsing and computed-value files through the style replay and the interpolation helpers through the engine, with expectations, exclusions and per-module floors beside the files.