CSS Layout API Level 1

A Collection of Interesting Ideas,

This version:
https://drafts.css-houdini.org/css-layout-api-1/
Feedback:
public-houdini@w3.org with subject line “[css-layout-api] … message topic …” (archives)
Issue Tracking:
GitHub
Inline In Spec
Editors:

Abstract

Status of this document

1. Introduction

The layout stage of CSS is responsible for generating and positioning fragments from the box tree.

This specification describes an API which allows developers to layout a box in response to computed style and box tree changes.

2. Layout API Containers

Name: display
New values: layout(<ident>) | inline-layout(<ident>)
layout()
This value causes an element to generate a block-level layout API container box.
inline-layout()
This value causes an element to generate an inline-level layout API container box.

A layout API container is the box generated by an element with a computed display of layout() or inline-layout().

A layout API container establishes a new layout API formatting context for its contents. This is the same as establishing a block formatting context, except that the layout provided by the author is used instead of the block layout. For example, floats do not intrude into the layout API container, and the layout API container’s margins do not collapse with the margins of its contents.

Layout API containers form a containing block for their contents exactly like block containers do. [CSS21]

Note: In a future level of the specification there may be a way to override the containing block behaviour.

The overflow property applies to layout API containers. This is discussed (TODO: writing about scrollbars).

As the layout is entirely up to the author, properties which are used in other layout modes (e.g. flex, block) may not apply. For example an author may not call respect the width or height properties.

If an element’s specified display is inline-layout(), then its display property computes to layout() in certain circumstances: the table in CSS 2.1 Section 9.7 is amended to contain an additional row, with inline-layout() in the "Specified Value" column and layout() in the "Computed Value" column.

A layout API container has a layout instance, initially this is set to null. This is an instance of the author defined layout class (see §4.3 Registering A Layout). If the box’s computed value of display changes, this must be reset to null.

Having the layout instance on the box is wrong, should really be a map on the layout worklet global scope.

2.1. Layout API Container Painting

Layout API Container children paint exactly the same as inline blocks [CSS21], except that the order in which they are returned from the layout method (via childFragments) is used in place of raw document order, and z-index values other than auto create a stacking context even if position is static.

3. Layout API Model and Terminology

This section gives an overview of the Layout API given to authors.

The current layout is the layout algorithm for the box we are currently performing layout for.

The parent layout is the layout algorithm for the box’s direct parent, (the layout algorithm which is requesting the current layout to be performed).

A child layout is the layout algorithm for a LayoutChild of the current layout.

3.1. Layout Children

[Exposed=LayoutWorklet]
interface LayoutChild {
    FragmentRequest layoutNextFragment(ConstraintSpace space, ChildBreakToken breakToken);
};

[Exposed=LayoutWorklet]
interface InlineLayoutChild : LayoutChild {
};

[Exposed=LayoutWorklet]
interface BoxLayoutChild : LayoutChild {
    readonly attribute StylePropertyMapReadOnly styleMap;
};

A LayoutChild represents either a CSS generated box or a sequence of non-atomic inline boxes before layout has occurred. (The box or boxes will all have a computed value of display that is not none).

The LayoutChild does not contain any layout information itself (like inline or block size) but can be used to generate Fragments which do contain layout information.

An author cannot construct a LayoutChild with this API, this happens at a separate stage of the rendering engine (post style resolution).

A InlineLayoutChild represents a sequence of non-atomic inlines. It does not have a single computed style associated with it as it may contain multiple inline boxes inside it with different computed style.

Note: As an example the following would be placed into a single InlineLayoutChild:
This is a next node, <span>with some additional styling,
that may</span> break over<br>multiple lines.

Multiple non-atomic inlines are placed within the same InlineLayoutChild to allow rendering engines to perform text shaping across element boundaries.

Note: As an example the following should produce one Fragment but is from three non-atomic inlines:
ع<span style="color: blue">ع</span>ع

Note: In a future level of the specification there may be a way to query the computed style of ranges inside a InlineLayoutChild.

A BoxLayoutChild represents a single box. It does have an associated computed style which can be accessed by styleMap. The styleMap will only contain properties which are listed in the child input properties array.

A BoxLayoutChild could be generated by:

Note: As an example the following would be placed into three BoxLayoutChildren:
<style>
  #box::before { content: 'hello!'; }
</style>
<div id="box">A block level box with text.</div>
<img src="..." />

An array of LayoutChildren is passed into the layout method which represents the children of the current box which is being laid out.

To perform layout on a box the author can invoke the layoutNextFragment() method. This will produce a Fragment which contains layout information.

The layoutNextFragment() method may be invoked multiple times with different arguments to query the LayoutChild for different layout information.

3.2. Layout Fragments

[Exposed=LayoutWorklet]
interface Fragment {
    readonly attribute double inlineSize;
    readonly attribute double blockSize;

    readonly attribute double inlineOverflowSize;
    readonly attribute double blockOverflowSize;

    attribute double inlineOffset;
    attribute double blockOffset;

    readonly attribute ChildBreakToken? breakToken;

    readonly attribute double alignmentBaseline;
};

A Fragment represents a CSS fragment of a LayoutChild after layout has occurred on that child. This is produced by the layoutNextFragment() method.

The Fragment has inlineSize and blockSize attributes, which are set by the respective child’s layout algorithm. They cannot be changed. If the current layout requires a different inlineSize or blockSize the author must perform layoutNextFragment() again with different arguments in order to get different results.

The Fragment has inlineOverflowSize and blockOverflowSize attributes. This is the size of the overflow area of the fragment. If the fragment didn’t overflow these attributes will be the same as inlineSize and blockSize respectively.

The author inside the current layout can position a resulting Fragment by setting its inlineOffset and blockOffset attributes. If not set by the author they default to zero.

The layout algorithm performs a block-like layout (positioning fragments sequentially in the block direction), while centering its children in the inline direction.
registerLayout('block-like', class extends Layout {
    static blockifyChildren = true;
    static inputProperties = super.inputProperties;

    *layout(space, children, styleMap) {
        const inlineSize = resolveInlineSize(space, styleMap);

        const bordersAndPadding = resolveBordersAndPadding(constraintSpace, styleMap);
        const scrollbarSize = resolveScrollbarSize(constraintSpace, styleMap);
        const availableInlineSize = inlineSize -
                                    bordersAndPadding.inlineStart -
                                    bordersAndPadding.inlineEnd -
                                    scrollbarSize.inline;

        const availableBlockSize = resolveBlockSize(constraintSpace, styleMap) -
                                   bordersAndPadding.blockStart -
                                   bordersAndPadding.blockEnd -
                                   scrollbarSize.block;

        const childFragments = [];
        const childConstraintSpace = new ConstraintSpace({
            inlineSize: availableInlineSize,
            blockSize: availableBlockSize,
        });

        let maxChildInlineSize = 0;
        let blockOffset = bordersAndPadding.blockStart;

        for (let child of children) {
            const fragment = yield child.layoutNextFragment(childConstraintSpace);

            // Position the fragment in a block like manner, centering it in the
            // inline direction.
            fragment.blockOffset = blockOffset;
            fragment.inlineOffset = Math.max(
                bordersAndPadding.inlineStart,
                (availableInlineSize - fragment.inlineSize) / 2);

            maxChildInlineSize =
                Math.max(maxChildInlineSize, childFragments.inlineSize);
            blockOffset += fragment.blockSize;
        }

        const inlineOverflowSize = maxChildInlineSize + bordersAndPadding.inlineEnd;
        const blockOverflowSize = blockOffset + bordersAndPadding.blockEnd;
        const blockSize = resolveBlockSize(
            constraintSpace, styleMap, blockOverflowSize);

        return {
            inlineSize: inlineSize,
            blockSize: blockSize,
            inlineOverflowSize: inlineOverflowSize,
            blockOverflowSize: blockOverflowSize,
            childFragments: childFragments,
        };
    }
});

The Fragment's breakToken specifies where the LayoutChild last fragmented. If the breakToken is null the LayoutChild wont produce any more Fragments for that token chain. The breakToken can be passed to the layoutNextFragment() function to produce the next Fragment for a particular child. The breakToken cannot be changed. If the current layout requires a different breakToken the author must perform layoutNextFragment() again with different arguments.

The Fragment's alignmentBaseline attribute specify where the alignment baseline is positioned relative to the block start of the fragment. It cannot be changed.

Note: In a future level of the specification there may be a way to query for additional baseline information, for example where the alphabetic or center baseline is positioned.

3.3. Constraint Spaces

[Constructor(optional ConstraintSpaceOptions options),Exposed=LayoutWorklet]
interface ConstraintSpace {
    readonly attribute double inlineSize;
    readonly attribute double blockSize;

    readonly attribute boolean inlineSizeFixed;
    readonly attribute boolean blockSizeFixed;

    readonly attribute boolean inlineShrinkToFit;

    readonly attribute double percentageInlineSize;
    readonly attribute double percentageBlockSize;

    readonly attribute boolean inlineOverflow;
    readonly attribute boolean blockOverflow;

    readonly attribute BlockFragmentationType blockFragmentationType;
};

dictionary ConstraintSpaceOptions {
    double inlineSize = Infinity;
    double blockSize = Infinity;

    boolean inlineSizeFixed = false;
    boolean blockSizeFixed = false;

    boolean inlineShrinkToFit = false;

    double? percentageInlineSize = null;
    double? percentageBlockSize = null;

    BlockFragmentationType blockFragmentationType = "none";
};

enum BlockFragmentationType { "none", "page", "column", "region" };

A ConstraintSpace is passed into the layout method which represents the available space for the current layout to perform layout inside. It is also used to pass information about the available space into a child layout.

The ConstraintSpace has inlineSize and blockSize attributes. This represents the available space for a Fragment which the layout should respect.

Note: Some layouts may need to produce a Fragment which exceed this size. For example a replaced element. The parent layout should expect this to occur and deal with it appropriately.

A parent layout may require the current layout to be exactly a particular size. If the inlineSizeFixed or blockSizeFixed are true the current layout should produce a Fragment with a fixed size in the appropriate direction.

The layout algorithm performs a flexbox-like distribution of spare space in the inline direction. It creates child constraint spaces which specify that a child should be a fixed inline size.
registerLayout('flex-distribution-like', class {
    *layout(space, children, styleMap, breakToken) {
        const inlineSize = resolveInlineSize(space, styleMap);

        const bordersAndPadding = resolveBordersAndPadding(constraintSpace, styleMap);
        const scrollbarSize = resolveScrollbarSize(constraintSpace, styleMap);
        const availableInlineSize = inlineSize -
                                    bordersAndPadding.inlineStart -
                                    bordersAndPadding.inlineEnd -
                                    scrollbarSize.inline;

        const availableBlockSize = resolveBlockSize(constraintSpace, styleMap) -
                                   bordersAndPadding.blockStart -
                                   bordersAndPadding.blockEnd -
                                   scrollbarSize.block;

        const unconstrainedSizes = [];
        const childConstraintSpace = new ConstraintSpace({
            inlineShrinkToFit: true,
            inlineSize: availableInlineSize,
            blockSize: availableBlockSize,
        });
        let totalSize = 0;

        // Calculate the unconstrained size for each child.
        for (let child of children) {
            const fragment = yield child.layoutNextFragment(childConstraintSpace);
            unconstrainedSizes.push(fragment.inlineSize);
            totalSize += fragment.inlineSize;
        }

        // Distribute spare space between children.
        const remainingSpace = Math.max(0, inlineSize - totalSize);
        const extraSpace = remainingSpace / children.length;
        const childFragments = [];
        let inlineOffset = 0;
        let maxChildBlockSize = 0;
        for (let i = 0; i < children.length; i++) {
            let fragment = yield child.layoutNextFragment(new ConstraintSpace({
                inlineSize: unconstrainedSizes[i] + extraSpace,
                inlineSizeFixed: true,
                blockSize: availableBlockSize
            }));

            fragment.inlineOffset = inlineOffset;
            inlineOffset += fragment.inlineSize;

            maxChildBlockSize = Math.max(maxChildBlockSize, fragment.blockSize);

            childFragments.push(fragment);
        }

        // Resolve our block size.
        const blockSize = resolveBlockSize(constraintSpace, styleMap, maxChildBlockSize);

        return {
            inlineSize: inlineSize,
            blockSize: blockSize,
            inlineOverflowSize: Math.max(inlineSize, totalSize),
            blockOverflowSize: maxChildBlockSize,
            childFragments: childFragments,
        };
    }
});

The ConstraintSpace has a inlineShrinkToFit attribute. This is used to indicate that the layout should treat auto as fit-content instead.

The ConstraintSpace has percentageInlineSize and percentageBlockSize attributes. These represent the size that a layout percentages should be resolved against while performing layout.

The ConstraintSpace has a blockFragmentationType attribute. The current layout should produce a Fragment which fragments at the blockSize if possible.

The current layout may choose not to fragment a LayoutChild based on the blockFragmentationType, for example if the child has a property like break-inside: avoid-page;.

3.4. Breaking and Fragmentation

[Exposed=LayoutWorklet]
interface ChildBreakToken {
    readonly attribute BreakType breakType;
    readonly attribute LayoutChild child;
};

[Exposed=LayoutWorklet]
interface BreakToken {
    readonly attribute sequence<ChildBreakToken> childBreakTokens;
    readonly attribute Object data;
};

dictionary BreakTokenOptions {
    sequence<ChildBreakToken> childBreakTokens;
    Object data = null;
};

enum BreakType { "none", "inline", "inline-hyphen", "column", "page", "region" };

Fill out other inline type break types.

A LayoutChild can produce multiple Fragments. A BoxLayoutChild may fragment in the block direction if a blockFragmentation is not none. A InlineLayoutChild may fragment in the inline direction.

A subsequent Fragment is produced by using the previous Fragment's breakToken. This tells the child layout to produce a Fragment starting at the point encoded in the ChildBreakToken.

Explain resuming the author defined layout.

This example shows a simple inline layout which places child fragments in the inline direction. It places each of its on a line, aligning their baselines.

This example also demonstrates using the previous breakToken of a Fragment to produce the next fragment for the LayoutChild.

It also demonstrates using the BreakToken to respect the ConstraintSpace's blockFragmentationType, it resumes it layout from the previous BreakToken. It returns a FragmentResultOptions with a breakToken which is used to resume the layout.

registerLayout('basic-inline', class extends Layout {
    static inputProperties = super.inputProperties;

    *layout(constraintSpace, children, styleMap, breakToken) {
        // Resolve our inline size.
        const inlineSize = resolveInlineSize(constraintSpace, styleMap);

        // Determine our (inner) available size.
        const bordersAndPadding =
            resolveBordersAndPadding(constraintSpace, styleMap);
        const scrollbarSize = resolveScrollbarSize(constraintSpace, styleMap);
        const availableInlineSize = inlineSize -
                                    bordersAndPadding.inlineStart -
                                    bordersAndPadding.inlineEnd -
                                    scrollbarSize.inline;

        const availableBlockSize = resolveBlockSize(constraintSpace, styleMap) -
                                   bordersAndPadding.blockStart -
                                   bordersAndPadding.blockEnd -
                                   scrollbarSize.block;

        const childFragments = [];
        let maxInlineSize = 0;

        let currentLine = [];
        let usedInlineSize = 0;
        let maxBaseline = 0;

        let lineOffset = 0;
        let maxLineBlockSize = 0;

        // Just a small little function which will update the above variables.
        const nextLine = function() {
            if (usedInlineSize > maxInlineSize) {
                maxInlineSize = usedInlineSize;
            }

            currentLine = [];
            usedInlineSize = 0;
            maxBaseline = 0;

            lineOffset += maxLineBlockSize;
            maxLineBlockSize = 0;
        }

        let childBreakToken = null;
        if (breakToken) {
            childBreakToken = breakToken.childBreakTokens[0];

            // Remove all the children we have already produced fragments for.
            children.splice(0, children.indexOf(childBreakToken.child));
        }

        let child = children.shift();
        while (child) {
            // Make sure we actually have space on the current line.
            if (usedInlineSize > availableInlineSize) {
                nextLine();
            }

            // The constraint space here will have the inline size of the
            // remaining space on the line.
            const remainingInlineSize = availableInlineSize - usedInlineSize;
            const constraintSpace = new ConstraintSpace({
                inlineSize: availableInlineSize - usedInlineSize,
                blockSize: availableBlockSize,
                percentageInlineSize: availableInlineSize,
                inlineShrinkToFit: true,
            });

            const fragment = yield child.layoutNextFragment(constraintSpace,
                                                            childBreakToken);
            childFragments.push(fragment);

            // Check if there is still space on the current line.
            if (fragment.inlineSize > remainingInlineSize) {
                nextLine();

                // Check if we have gone over the block fragmentation limit.
                if (constraintSpace.blockFragmentationType != 'none' &&
                    lineOffset > constraintSpace.blockSize) {
                    break;
                }
            }

            // Insert fragment on the current line.
            currentLine.push(fragment);
            fragment.inlineOffset = usedInlineSize;

            if (fragment.alignmentBaseline > maxBaseline) {
                maxBaseline = fragment.alignmentBaseline;
            }

            // Go through each of the fragments on the line and update their
            // block offsets.
            for (let fragmentOnLine of currentLine) {
                fragmentOnLine.blockOffset = lineOffset +
                    maxBaseline - fragmentOnLine.alignmentBaseline;

                const lineBlockSize =
                    fragmentOnLine.blockOffset + fragmentOnLine.blockSize;
                if (maxLineBlockSize < lineBlockSize) {
                    maxLineBlockSize = lineBlockSize;
                }
            }

            if (fragment.breakToken) {
                childBreakToken = fragment.breakToken;
            } else {
                // If a fragment doesn’t have a break token, we move onto the
                // next child.
                child = children.shift();
                childBreakToken = null;
            }
        }

        // Determine our block size.
        nextLine();
        const blockOverflowSize = lineOffset +
                                  bordersAndPadding.blockStart +
                                  bordersAndPadding.blockEnd;
        const blockSize = resolveBlockSize(constraintSpace,
                                           styleMap,
                                           blockOverflowSize);

        // Return our fragment.
        const result = {
            inlineSize: inlineSize,
            blockSize: blockSize,
            inlineOverflowSize: maxInlineSize,
            blockOverflowSize: blockOverflowSize,
            childFragments: childFragments,
        }

        if (childBreakToken) {
            result.breakToken = {
                childBreakTokens: [childBreakToken],
            };
        }

        return result;
    }
});

3.5. Utility Functions

[Exposed=LayoutWorklet]
interface LayoutStrut {
  readonly attribute double inlineStart;
  readonly attribute double inlineEnd;

  readonly attribute double blockStart;
  readonly attribute double blockEnd;
};

[Exposed=LayoutWorklet]
interface LayoutSize {
  readonly attribute double inline;
  readonly attribute double block;
};

partial interface LayoutWorkletGlobalScope {
    double resolveInlineSize(ConstraintSpace constraintSpace,
                             StylePropertyMapReadOnly styleMap);

    double resolveBlockSize(ConstraintSpace constraintSpace,
                            StylePropertyMapReadOnly styleMap,
                            optional double contentSize);

    LayoutStrut resolveBordersAndPadding(ConstraintSpace constraintSpace,
                                         StylePropertyMapReadOnly styleMap);

    LayoutSize resolveScrollbarSize(ConstraintSpace constraintSpace,
                                    StylePropertyMapReadOnly styleMap);
};

[Exposed=LayoutWorklet]
interface Layout {
    readonly attribute sequence<DOMString> inputProperties;
    readonly attribute sequence<DOMString> childInputProperties;
};

Specify the behaviour of these functions.

4. Layout

This section describes how the CSS Layout API interacts with the user agent’s layout engine.

4.1. Layout Invalidation

A document has an associated layout name to input properties map and a layout name to child input properties map. Initially these maps are empty and are populated when registerLayout(name, layoutCtor) is called.

Each box has an associated layout valid flag. It may be either layout-valid or layout-invalid. It is initially set to layout-invalid.

The above flag is too restrictive on user agents, change.

When the computed style for a box changes, the user agent must run the following steps:

  1. Let layoutFunction be the <layout()> or <inline-layout()> function of the display property on the computed style for the box if it exists. If it is a different type of value (e.g. grid) then abort all these steps.

  2. Let name be the first argument of the layoutFunction.

  3. Let inputProperties be the result of looking up name on layout name to input properties map.

  4. Let childInputProperties be the result of looking up name on layout name to child input properties map.

  5. For each property in inputProperties, if the property’s computed value has changed, set the layout valid flag on the box to layout-invalid.

  6. For each property in childInputProperties, if the property’s computed value has changed, set the layout valid flag on the box to layout-invalid.

When a child box represented by a BoxLayoutChild is added or removed from the box tree or has its layout invalidated (from a computed style change). Set the layout valid flag on the current box to layout-invalid.

When a child non-atomic inline represented by a InlineLayoutChild is added or removed from the box tree or has its layout invalidated (from a computed style change, or if its text has changed). Set the layout valid flag on the current box to layout-invalid.

Note: This only describes layout invalidation as it relates to the CSS Layout API. All boxes conceptually have a layout valid flag and these changes are propagated through the box tree.

4.2. Layout Worklet

The layoutWorklet attribute allows access to the Worklet responsible for all the classes which are related to layout.

The layoutWorklet's worklet global scope type is LayoutWorkletGlobalScope.

partial interface CSS {
    [SameObject] readonly attribute Worklet layoutWorklet;
};

The LayoutWorkletGlobalScope is the global execution context of the layoutWorklet.

[Global=(Worklet,LayoutWorklet),Exposed=LayoutWorklet]
interface LayoutWorkletGlobalScope : WorkletGlobalScope {
    void registerLayout(DOMString name, VoidFunction layoutCtor);
};

4.3. Registering A Layout

A layout definition describes an author defined layout which can be referenced by the <layout()> or <inline-layout()> functions. It consists of:

The LayoutWorkletGlobalScope has a map of layout name to layout definition map. Initially this map is empty; it is populated when registerLayout(name, layoutCtor) is called.

When the registerLayout(name, layoutCtor) method is called, the user agent must run the following steps:

  1. If the name exists as a key in the layout name to layout definition map, throw a NotSupportedError and abort all these steps.

  2. If the name is an empty string, throw a TypeError and abort all these steps.

  3. Let inputProperties be the result of Get(layoutCtor, "inputProperties").

  4. If inputProperties is not undefined, and the result of IsArray(inputProperties) is false, throw a TypeError and abort all these steps.

    If inputProperties is undefined, let inputProperties be a new empty array.

  5. For each item in inputProperties perform the following substeps:

    1. If the result of Type(item) is not String, throw a TypeError and abort all these steps.

  6. Let childInputProperties be the result of Get(layoutCtor, "childInputProperties").

  7. If childInputProperties is not undefined, and the result of IsArray(childInputProperties) is false, throw a TypeError and abort all these steps.

    If childInputProperties is undefined, let childInputProperties be a new empty array.

  8. For each item in childInputProperties perform the following substeps:

    1. If the result of Type(item) is not String, throw a TypeError and abort these steps.

Note: The list of CSS properties provided by "inputProperties" or "childInputProperties" can either by custom or native CSS properties.

Note: The list of CSS properties may contain shorthands.

Note: In order for a layout class to be forwards compatible, the list of CSS properties can also contain currently invalid properties for the user agent. For example margin-bikeshed-property.

  1. Let prototype be the result of Get(layoutCtor, "prototype").

  2. If the result of Type(prototype) is not Object, throw a TypeError and abort all these steps.

  3. Let layout be the result of Get(prototype, "layout").

  4. If the result of IsCallable(layout) is false, throw a TypeError and abort all these steps.

  5. If layout’s [[FunctionKind]] internal slot is not "generator", throw a TypeError and abort all these steps.

  6. Let definition be a new layout definition with:

  7. Add the key-value pair (name - inputProperties) to the layout name to input properties map of the associated document.

  8. Add the key-value pair (name - childInputProperties) to the layout name to child input properties map of the associated document.

  9. Add the key-value pair (name - definition) to the layout name to layout definition map of the associated document.

Note: The shape of the class should be:
class MyLayout {
    static get inputProperties() { return ['--foo']; }
    static get childrenInputProperties() { return ['--bar']; }

    *layout(constraintSpace, children, styleMap, breakToken) {
        // Layout code goes here.
    }
}

4.4. Layout Engine

[Exposed=LayoutWorklet]
interface FragmentRequest {
  // Has internal slots:
  // [[layoutChild]] - The layout child to generate the fragment for.
  // [[constraintSpace]] - The constraint space to perform layout in.
  // [[breakToken]] - The break token to resume the layout with.
};

The layout method on the author supplied layout class is a generator function instead of a regular javascript function. This is for user-agents to be able to support asynchronous and parallel layout engines.

When an author invokes the layoutNextFragment() method on a LayoutChild the user-agent doesn’t synchronously generate a Fragment to return to the author’s code. Instead it returns a FragmentRequest. This is a completely opaque object to the author but contains internal slots which encapsulates the layoutNextFragment() method call.

When a FragmentRequest(s) are yielded from a layout generator object the user-agent’s layout engine may run the algorithm asynchronously with other work, and/or on a different thread of execution. When Fragment(s) have been produced by the engine, the user-agent will tick the generator object with the resulting Fragment(s).

An example layout engine written in javascript is shown below.
class LayoutEngine {
  // This function takes the root of the box-tree, a ConstraintSpace, and a
  // BreakToken to (if paginating for printing for example) and generates a
  // Fragment.
  layoutEntry(rootBox, rootPageConstraintSpace, breakToken) {
    return layoutFragment({
      box: rootBox,
      constraintSpace: rootPageConstraintSpace,
      breakToken: breakToken,
    });
  }

  // This function takes a FragmentRequest and calls the appropriate layout
  // algorithm to generate the a Fragment.
  layoutFragment(fragmentRequest) {
    const box = fragmentRequest.box;
    const algorithm = selectLayoutAlgorithmForBox(box);
    const fragmentRequestGenerator = algorithm.layout(
        fragmentRequest.constraintSpace,
        box.children,
        box.styleMap,
        fragmentRequest.breakToken);

    let nextFragmentRequest = fragmentRequestGenerator.next();

    while (!nextFragmentRequest.done) {
      // A user-agent may decide to perform layout to generate the fragments in
      // parallel on separate threads. This example performs them synchronously
      // in order.
      let fragments = nextFragmentRequest.value.map(layoutFragment);

      // A user-agent may decide to yield for other work (garbage collection for
      // example) before resuming this layout work. This example just performs
      // layout synchronously without any ability to yield.
      nextFragmentRequest = fragmentRequestGenerator.next(fragments);
    }

    return nextFragmentRequest.value; // Return the final Fragment.
  }
}

TODO explain parallel layout + FragmentRequest, etc.

4.5. Performing Layout

// This is the final return value from the author defined layout() method.
dictionary FragmentResultOptions {
    double inlineSize = 0;
    double blockSize = 0;
    double inlineOverflowSize = null;
    double blockOverflowSize = null;
    sequence<Fragment> childFragments = [];
    BreakTokenOptions breakToken = null;
    double alignmentBaseline = null;
};

Specify how we do min/max content contributions.

Need to specify that the LayoutChild objects should remain the same between layouts so the author can store information? Not sure.

When the user agent wants to generate a layout API fragment of a layout API formatting context for a given box, constraintSpace, children and an optional breakToken it must run the following steps:

  1. If the layout valid flag for the box is layout-valid the user agent may use a fragment from a previous invocation of this algorithm if the box, constraintSpace, children and optional breakToken are the same. If so it may abort all these steps and use the cached fragment.

    The above is too limiting wrt. the layout valid flag. Need to separate out the produce the fragment step, with the cache invalidation.

    Note: The user agent for implementation reasons may also continue with all these steps in this case. It can do this every frame, or multiple times per frame.

  2. Let layoutFunction be the <layout()> or <inline-layout()> for the computed value of display on the box.

  3. Let name be the first argument of the layoutFunction.

  4. Let workletGlobalScope be a LayoutWorkletGlobalScope from the list of worklet’s WorkletGlobalScopes from the layout Worklet.

    The user agent may also create a WorkletGlobalScope given the layout Worklet and use that.

    Note: The user agent may use any policy for which LayoutWorkletGlobalScope to select or create. It may use a single LayoutWorkletGlobalScope or multiple and randomly assign between them.

  5. Let definition be the result of looking up name on the workletGlobalScope’s layout name to layout definition map.

    If definition does not exist, let the fragment output be an invalid fragment and abort all these steps.

    Define what an "invalid fragment" is.

  6. Let layoutInstance be the result of looking up the layout instance on the box. If layoutInstance is null run the following substeps.

    1. If the layout class constructor valid flag on definition is false, let the fragment output be an invalid fragment and abort all these steps.

    2. Let layoutCtor be the layout class constructor on definition.

    3. Let layoutInstance be the result of Construct(layoutCtor).

      If Construct throws an exception, set the definition’s layout class constructor valid flag to false, let the fragment output be an invalid fragment and abort all these steps.

    4. Set layout instance on box to layoutInstance.

    Note: Layout instance will be set to null whenever the computed style of display on box changes.

  7. Let layoutGeneratorFunction be the result of looking up the layout generator function.

  8. Let inputProperties be the result of looking up name on the associated document’s layout name to input properties map.

  9. Let styleMap be a new StylePropertyMapReadOnly populated with only the computed value’s for properties listed in inputProperties.

  10. Let layoutGenerator be the result of Call(layoutGeneratorFunction, layoutInstance, «constraintSpace, children, styleMap, breakToken»).

  11. Let childFragmentResults be «» (the empty list).

  12. Let nextResult be the result of calling Invoke(next, layoutGenerator, childFragmentResults).

  13. Perform the following substeps until the result of Get(nextResult, "done") is true.

    1. Set childFragmentResults be «» (the empty list).

    2. Let fragmentRequests be the result of Get(nextResult, "value").

    3. For each fragmentRequest in fragmentRequests perform the following substeps:

      1. Let layoutChild be result of looking up the internal slot [[layoutChild]] on fragmentRequest.

      2. Let childConstraintSpace be the result of looking up the internal slot [[childConstraintSpace]] on fragmentRequest.

      3. Let childBreakToken be the result of looking up the internal slot [[childBreakToken]] on fragmentRequest.

      4. Let childFragmentResult be the result of invoking generate a fragment with the arguments layoutChild, childConstraintSpace, childBreakToken.

        The user agent may perform this step in parallel.

      5. Append childFragmentResult to childFragmentResults.

    4. Let nextResult be the result of calling Invoke(next, layoutGenerator, childFragmentResults).

  14. Let fragmentResult be the result of calling Get(nextResult, "value").

  15. Let fragment be a fragment with the following properties:

  1. Return fragment.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Advisements are normative sections styled to evoke special attention and are set apart from other normative text with <strong class="advisement">, like this: UAs MUST provide an accessible alternative.

Conformance classes

Conformance to this specification is defined for three conformance classes:

style sheet
A CSS style sheet.
renderer
A UA that interprets the semantics of a style sheet and renders documents that use them.
authoring tool
A UA that writes a style sheet.

A style sheet is conformant to this specification if all of its statements that use syntax defined in this module are valid according to the generic CSS grammar and the individual grammars of each feature defined in this module.

A renderer is conformant to this specification if, in addition to interpreting the style sheet as defined by the appropriate specifications, it supports all the features defined by this specification by parsing them correctly and rendering the document accordingly. However, the inability of a UA to correctly render a document due to limitations of the device does not make the UA non-conformant. (For example, a UA is not required to render color on a monochrome monitor.)

An authoring tool is conformant to this specification if it writes style sheets that are syntactically correct according to the generic CSS grammar and the individual grammars of each feature in this module, and meet all other conformance requirements of style sheets as described in this module.

Partial implementations

So that authors can exploit the forward-compatible parsing rules to assign fallback values, CSS renderers must treat as invalid (and ignore as appropriate) any at-rules, properties, property values, keywords, and other syntactic constructs for which they have no usable level of support. In particular, user agents must not selectively ignore unsupported component values and honor supported values in a single multi-value property declaration: if any value is considered invalid (as unsupported values must be), CSS requires that the entire declaration be ignored.

Implementations of Unstable and Proprietary Features

To avoid clashes with future stable CSS features, the CSSWG recommends following best practices for the implementation of unstable features and proprietary extensions to CSS.

Non-experimental implementations

Once a specification reaches the Candidate Recommendation stage, non-experimental implementations are possible, and implementors should release an unprefixed implementation of any CR-level feature they can demonstrate to be correctly implemented according to spec.

To establish and maintain the interoperability of CSS across implementations, the CSS Working Group requests that non-experimental CSS renderers submit an implementation report (and, if necessary, the testcases used for that implementation report) to the W3C before releasing an unprefixed implementation of any CSS features. Testcases submitted to W3C are subject to review and correction by the CSS Working Group.

Further information on submitting testcases and implementation reports can be found from on the CSS Working Group’s website at http://www.w3.org/Style/CSS/Test/. Questions should be directed to the public-css-testsuite@w3.org mailing list.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-ALIGN-3]
Elika Etemad; Tab Atkins Jr.. CSS Box Alignment Module Level 3. URL: https://www.w3.org/TR/css-align-3/
[CSS-BREAK-3]
Rossen Atanassov; Elika Etemad. CSS Fragmentation Module Level 3. URL: https://www.w3.org/TR/css-break-3/
[CSS-CASCADE-4]
Elika Etemad; Tab Atkins Jr.. CSS Cascading and Inheritance Level 4. URL: https://www.w3.org/TR/css-cascade-4/
[CSS-DISPLAY-3]
Elika Etemad. CSS Display Module Level 3. URL: https://www.w3.org/TR/css-display-3/
[CSS-GRID-1]
Tab Atkins Jr.; Elika Etemad; Rossen Atanassov. CSS Grid Layout Module Level 1. URL: https://www.w3.org/TR/css-grid-1/
[CSS-OVERFLOW-3]
David Baron; Florian Rivoal. CSS Overflow Module Level 3. URL: https://www.w3.org/TR/css-overflow-3/
[CSS-PAGE-3]
CSS Paged Media Module Level 3 URL: https://www.w3.org/TR/css3-page/
[CSS-POSITION-3]
Rossen Atanassov; Arron Eicholz. CSS Positioned Layout Module Level 3. URL: https://www.w3.org/TR/css-position-3/
[CSS-PSEUDO-4]
Daniel Glazman; Elika Etemad; Alan Stearns. CSS Pseudo-Elements Module Level 4. URL: https://www.w3.org/TR/css-pseudo-4/
[CSS-SIZING-3]
Elika Etemad. CSS Intrinsic & Extrinsic Sizing Module Level 3. URL: https://www.w3.org/TR/css-sizing-3/
[CSS-TYPED-OM-1]
Shane Stephens. CSS Typed OM Level 1. URL: https://www.w3.org/TR/css-typed-om-1/
[CSS-VALUES-3]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 3. URL: https://www.w3.org/TR/css-values-3/
[CSS-WRITING-MODES-4]
CSS Writing Modes Module Level 4 URL: https://drafts.csswg.org/css-writing-modes-4/
[CSS21]
Bert Bos; et al. Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification. 7 June 2011. REC. URL: https://www.w3.org/TR/CSS2
[CSS22]
Bert Bos. Cascading Style Sheets Level 2 Revision 2 (CSS 2.2) Specification. URL: https://www.w3.org/TR/CSS22/
[CSSOM-1]
Simon Pieters; Glenn Adams. CSS Object Model (CSSOM). URL: https://www.w3.org/TR/cssom-1/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[SERVICE-WORKERS-1]
Alex Russell; et al. Service Workers 1. URL: https://www.w3.org/TR/service-workers-1/
[WebIDL]
Cameron McCormack; Boris Zbarsky; Tobie Langel. Web IDL. URL: https://heycam.github.io/webidl/
[WORKLETS-1]
Ian Kilpatrick. Worklets Level 1. URL: https://www.w3.org/TR/worklets-1/

Property Index

No properties defined.

IDL Index

[Exposed=LayoutWorklet]
interface LayoutChild {
    FragmentRequest layoutNextFragment(ConstraintSpace space, ChildBreakToken breakToken);
};

[Exposed=LayoutWorklet]
interface InlineLayoutChild : LayoutChild {
};

[Exposed=LayoutWorklet]
interface BoxLayoutChild : LayoutChild {
    readonly attribute StylePropertyMapReadOnly styleMap;
};

[Exposed=LayoutWorklet]
interface Fragment {
    readonly attribute double inlineSize;
    readonly attribute double blockSize;

    readonly attribute double inlineOverflowSize;
    readonly attribute double blockOverflowSize;

    attribute double inlineOffset;
    attribute double blockOffset;

    readonly attribute ChildBreakToken? breakToken;

    readonly attribute double alignmentBaseline;
};

[Constructor(optional ConstraintSpaceOptions options),Exposed=LayoutWorklet]
interface ConstraintSpace {
    readonly attribute double inlineSize;
    readonly attribute double blockSize;

    readonly attribute boolean inlineSizeFixed;
    readonly attribute boolean blockSizeFixed;

    readonly attribute boolean inlineShrinkToFit;

    readonly attribute double percentageInlineSize;
    readonly attribute double percentageBlockSize;

    readonly attribute boolean inlineOverflow;
    readonly attribute boolean blockOverflow;

    readonly attribute BlockFragmentationType blockFragmentationType;
};

dictionary ConstraintSpaceOptions {
    double inlineSize = Infinity;
    double blockSize = Infinity;

    boolean inlineSizeFixed = false;
    boolean blockSizeFixed = false;

    boolean inlineShrinkToFit = false;

    double? percentageInlineSize = null;
    double? percentageBlockSize = null;

    BlockFragmentationType blockFragmentationType = "none";
};

enum BlockFragmentationType { "none", "page", "column", "region" };

[Exposed=LayoutWorklet]
interface ChildBreakToken {
    readonly attribute BreakType breakType;
    readonly attribute LayoutChild child;
};

[Exposed=LayoutWorklet]
interface BreakToken {
    readonly attribute sequence<ChildBreakToken> childBreakTokens;
    readonly attribute Object data;
};

dictionary BreakTokenOptions {
    sequence<ChildBreakToken> childBreakTokens;
    Object data = null;
};

enum BreakType { "none", "inline", "inline-hyphen", "column", "page", "region" };

[Exposed=LayoutWorklet]
interface LayoutStrut {
  readonly attribute double inlineStart;
  readonly attribute double inlineEnd;

  readonly attribute double blockStart;
  readonly attribute double blockEnd;
};

[Exposed=LayoutWorklet]
interface LayoutSize {
  readonly attribute double inline;
  readonly attribute double block;
};

partial interface LayoutWorkletGlobalScope {
    double resolveInlineSize(ConstraintSpace constraintSpace,
                             StylePropertyMapReadOnly styleMap);

    double resolveBlockSize(ConstraintSpace constraintSpace,
                            StylePropertyMapReadOnly styleMap,
                            optional double contentSize);

    LayoutStrut resolveBordersAndPadding(ConstraintSpace constraintSpace,
                                         StylePropertyMapReadOnly styleMap);

    LayoutSize resolveScrollbarSize(ConstraintSpace constraintSpace,
                                    StylePropertyMapReadOnly styleMap);
};

[Exposed=LayoutWorklet]
interface Layout {
    readonly attribute sequence<DOMString> inputProperties;
    readonly attribute sequence<DOMString> childInputProperties;
};

partial interface CSS {
    [SameObject] readonly attribute Worklet layoutWorklet;
};

[Global=(Worklet,LayoutWorklet),Exposed=LayoutWorklet]
interface LayoutWorkletGlobalScope : WorkletGlobalScope {
    void registerLayout(DOMString name, VoidFunction layoutCtor);
};

[Exposed=LayoutWorklet]
interface FragmentRequest {
  // Has internal slots:
  // [[layoutChild]] - The layout child to generate the fragment for.
  // [[constraintSpace]] - The constraint space to perform layout in.
  // [[breakToken]] - The break token to resume the layout with.
};

// This is the final return value from the author defined layout() method.
dictionary FragmentResultOptions {
    double inlineSize = 0;
    double blockSize = 0;
    double inlineOverflowSize = null;
    double blockOverflowSize = null;
    sequence<Fragment> childFragments = [];
    BreakTokenOptions breakToken = null;
    double alignmentBaseline = null;
};

Issues Index

Having the layout instance on the box is wrong, should really be a map on the layout worklet global scope.
Fill out other inline type break types.
Explain resuming the author defined layout.
Specify the behaviour of these functions.
The above flag is too restrictive on user agents, change.
Specify how we do min/max content contributions.
Need to specify that the LayoutChild objects should remain the same between layouts so the author can store information? Not sure.
The above is too limiting wrt. the layout valid flag. Need to separate out the produce the fragment step, with the cache invalidation.
Define what an "invalid fragment" is.