Title: CSS Layout API Level 1
Status: DREAM
Group: houdini
ED: https://drafts.css-houdini.org/css-layout-api-1/
Shortname: css-layout-api
Level: 1
Abstract:
Editor: Greg Whitworth, gwhit@microsoft.com
Editor: Ian Kilpatrick, ikilpatrick@chromium.org
Editor: Tab Atkins, jackalmage@gmail.com
Editor: Shane Stephens, shanestephens@google.com
Editor: Robert O'Callahan, robert@ocallahan.org
Editor: Rossen Atanassov, rossen.atanassov@microsoft.com
spec:css-break-3; type:dfn; text:fragment
spec:css-display-3; type:dfn; text:box
spec:css-display-3; type:value; for:display; text:none
spec:dom; type:dfn; text:element
urlPrefix: https://heycam.github.io/webidl/; type: dfn;
text: NotSupportedError
urlPrefix: #dfn-;
text: callback this value
text: exception
text: throw
url: throw; text: thrown
url: es-invoking-callback-functions; text: Invoke
urlPrefix: https://tc39.github.io/ecma262/#sec-; type: dfn;
text: constructor
text: Construct
text: IsArray
text: IsCallable
text: IsConstructor
text: HasProperty
url: get-o-p; text: Get
url: terms-and-definitions-function; text: function
urlPrefix: native-error-types-used-in-this-standard-
text: TypeError
Introduction {#intro}
=====================
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.
Layout API Containers {#layout-api-containers}
==============================================
Name: display
New values: layout(<>) | inline-layout(<>)
- 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 [[#registering-layout]]). If the box's
computed value of 'display' changes, this must be reset to null.
Issue: Having the layout instance on the box is wrong, should really be a map on the layout worklet
global scope.
Layout API Container Painting {#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
{{FragmentResultOptions/childFragments}}) is used in place of raw document order, and 'z-index'
values other than ''z-index/auto'' create a stacking context even if 'position' is ''static''.
Layout API Model and Terminology {#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.
Layout Children {#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 {{Fragment}}s 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 asscessed by {{BoxLayoutChild/styleMap}}. The {{BoxLayoutChild/styleMap}} will only contain
properties which are listed in the child input properties array.
A {{BoxLayoutChild}} could be generated by:
- An element.
- A ::before or ::after pseudo-element.
Note: Other pseudo-elements such as ::first-letter or ::first-line do not generate
a {{BoxLayoutChild}} for layout purposes. They are additional styling information for a text
node.
- An anonymous box. For example an anonymous box may be inserted as a result of:
- A Text node which has undergone blockification. (Or more generally a
{{InlineLayoutChild}} which has undergone blockification).
- An element with ''display: table-cell'' which doesn't have a parent with ''display: table''.
- An atomic inline.
Note: As an example the following would be placed into three {{BoxLayoutChild}}ren:
<style>
#box::before { content: 'hello!'; }
</style>
<div id="box">A block level box with text.</div>
<img src="..." />
An array of {{LayoutChild}}ren 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 {{LayoutChild/layoutNextFragment()}} method.
This will produce a {{Fragment}} which contains layout information.
The {{LayoutChild/layoutNextFragment()}} method may be invoked multiple times with different
arguments to query the {{LayoutChild}} for different layout information.
Layout Fragments {#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 dominantBaseline;
};
A {{Fragment}} represents a CSS fragment of a {{LayoutChild}} after layout has occurred on
that child. This is produced by the {{LayoutChild/layoutNextFragment()}} method.
The {{Fragment}} has {{Fragment/inlineSize}} and {{Fragment/blockSize}} attributes, which are set by
the respective child's layout algorithm. They cannot be changed. If the current layout
requires a different {{Fragment/inlineSize}} or {{Fragment/blockSize}} the author must perform
{{LayoutChild/layoutNextFragment()}} again with different arguments in order to get different
results.
The {{Fragment}} has {{Fragment/inlineOverflowSize}} and {{Fragment/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 {{Fragment/inlineSize}} and {{Fragment/blockSize}} respectively.
The author inside the current layout can position a resulting {{Fragment}} by setting its
{{Fragment/inlineOffset}} and {{Fragment/blockOffset}} attributes. If not set by the author they
default to zero.
The layout algorithm performs a block-like layout (positioning fragments sequentically 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 {{Fragment/breakToken}} specifies where the {{LayoutChild}} last fragmented. If
the {{Fragment/breakToken}} is null the {{LayoutChild}} wont produce any more {{Fragment}}s for that
token chain. The {{Fragment/breakToken}} can be passed to the {{LayoutChild/layoutNextFragment()}}
function to produce the next {{Fragment}} for a particular child. The {{Fragment/breakToken}} cannot
be changed.
If the current layout requires a different {{Fragment/breakToken}} the author must perform
{{LayoutChild/layoutNextFragment()}} again with different arguments.
The {{Fragment}}'s {{Fragment/dominantBaseline}} attribute specify where the dominant 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.
Constraint Spaces {#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 {{ConstraintSpace/inlineSize}} and {{ConstraintSpace/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 {{ConstraintSpace/inlineSizeFixed}} or {{ConstraintSpace/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 {{ConstraintSpace/inlineShrinkToFit}} attribute. This is used to
indicate that the layout should treat ''auto'' as ''fit-content'' instead.
The {{ConstraintSpace}} has {{ConstraintSpace/percentageInlineSize}} and
{{ConstraintSpace/percentageBlockSize}} attributes. These represent the size that a layout
percentages should be resolved against while performing layout.
The {{ConstraintSpace}} has a {{ConstraintSpace/blockFragmentationType}} attribute. The current
layout should produce a {{Fragment}} which fragments at the {{ConstraintSpace/blockSize}} if
possible.
The current layout may choose not to fragment a {{LayoutChild}} based on the
{{ConstraintSpace/blockFragmentationType}}, for example if the child has a property like
''break-inside: avoid-page;''.
Breaking and Fragmentation {#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" };
Issue: Fill out other inline type break types.
A {{LayoutChild}} can produce multiple {{Fragment}}s. A {{BoxLayoutChild}} may fragment in the block
direction if a {{ConstraintSpace/blockFragmentation}} is not none. A {{InlineLayoutChild}} may
fragment in the inline direction.
A subsequent {{Fragment}} is produced by using the previous {{Fragment}}'s {{Fragment/breakToken}}.
This tells the child layout to produce a {{Fragment}} starting at the point encoded in the
{{ChildBreakToken}}.
Issue: 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 dominant baselines.
This example also demonstrates using the previous {{Fragment/breakToken}} of a {{Fragment}} to
produce the next fragment for the {{LayoutChild}}.
It also demonstrates using the {{BreakToken}} to respect the {{ConstraintSpace}}'s
{{ConstraintSpace/blockFragmentationType}}, it resumes it layout from the previous {{BreakToken}}.
It returns a {{FragmentResultOptions}} with a {{FragmentResultOptions/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.dominantBaseline > maxBaseline) {
maxBaseline = fragment.dominantBaseline;
}
// Go through each of the fragments on the line and update their
// block offsets.
for (let fragmentOnLine of currentLine) {
fragmentOnLine.blockOffset = lineOffset +
maxBaseline - fragmentOnLine.dominantBaseline;
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;
}
});
Utility Functions {#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;
};
Issue: Specify the behaviour of these functions.
Layout {#layout}
================
This section describes how the CSS Layout API interacts with the user agent's layout engine.
Layout Invalidation {#layout-invalidation}
------------------------------------------
A document has an associated layout name to input properties map and a layout
name to child input properties map. Initialy 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.
Issue: 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 <> or <> 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 invalidatation as it relates to the CSS Layout API. All
boxes conceptually have a layout valid flag and these changes are propogated
through the box tree.
Layout Worklet {#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);
};
Registering A Layout {#registering-layout}
------------------------------------------
A layout definition describes an author defined layout which can be referenced by the
<> or <> functions. It consists of:
- A layout name.
- A layout class constructor which is the class constructor.
- A layout generator function which is the layout generator function callback.
- A layout class constructor valid flag.
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
.
9. Let |prototype| be the result of Get(|layoutCtor|, "prototype"
).
10. If the result of Type(|prototype|) is not Object, throw a TypeError and
abort all these steps.
11. Let |layout| be the result of Get(|prototype|, "layout"
).
12. If the result of IsCallable(|layout|) is false, throw a TypeError and
abort all these steps.
13. If |layout|'s \[[FunctionKind]]
internal slot is not "generator"
,
throw a TypeError and abort all these steps.
14. Let |definition| be a new layout definition with:
- layout name being |name|.
- layout class constructor being |layoutCtor|.
- layout generator function being |layout|.
- layout class constructor valid flag being true
15. Add the key-value pair (|name| - |inputProperties|) to the layout name to input
properties map of the associated document.
16. Add the key-value pair (|name| - |childInputProperties|) to the layout name to child
input properties map of the associated document.
17. 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.
}
}
Layout Engine {#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 {{LayoutChild/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 {{LayoutChild/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.
Performing Layout {#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 dominantBaseline = null;
};
Issue: Specify how we do min/max content contributions.
Issue: 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.
Issue: 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 <> or <> 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.
Issue: 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|»).
12. Let |childFragmentResults| be «» (the empty list).
11. Let |nextResult| be the result of calling Invoke(next
,
|layoutGenerator|, |childFragmentResults|).
12. 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|).
13. Let |fragmentResult| be the result of calling Get(|nextResult|,
"value"
).
14. Let |fragment| be a fragment with the following properties:
- The border box inline size is set to |fragmentResult|'s
{{FragmentResultOptions/inlineSize}}.
- The border box block size is set to |fragmentResult|'s
{{FragmentResultOptions/blockSize}}.
- The inline overflow size is set to |fragmentResult|'s
{{FragmentResultOptions/inlineOverflowSize}} if not null, otherwise it is set to
{{FragmentResultOptions/inlineSize}}.
- The block overflow size is set to |fragmentResult|'s
{{FragmentResultOptions/blockOverflowSize}} if not null, otherwise it is set to
{{FragmentResultOptions/blockSize}}.
If the |constraintSpace|'s {{ConstraintSpace/inlineOverflow}} is false
and
the inline overflow size is greater than the inline size and the computed
value for inline 'overflow' is ''auto'' then set |constraintSpace|'s
{{ConstraintSpace/inlineOverflow}} to true
.
If the |constraintSpace|'s {{ConstraintSpace/blockOverflow}} is false
and the
block overflow size is greater than the block size and the computed
value for block 'overflow' is ''auto'' then set |constraintSpace|'s
{{ConstraintSpace/blockOverflow}} to true
.
If either {{ConstraintSpace/inlineOverflow}} or {{ConstraintSpace/blockOverflow}} were set
in the above steps, restart this algorithm with the updated |constraintSpace|.
Note: In a future level of the specification there may be a way to more efficiently abort
a layout given a "scroll trigger line" on the constraint space.
- The children fragments of the |fragment| is set from |fragmentResult|'s
{{FragmentResultOptions/childFragments}}. The ordering is important as this is
dictates their paint order (described in [[#layout-api-containers]]). Their position
relative to the border box of the |fragment| should be based off the author
specified {{Fragment/inlineOffset}} and {{Fragment/blockOffset}}.
- The fragmentation break is set to |fragmentResult|'s
{{FragmentResultOptions/breakToken}}.
- The dominant baseline is set to |fragmentResult|'s
{{FragmentResultOptions/dominantBaseline}} if not null, otherwise it is set to:
- The {{Fragment/dominantBaseline}} of the first child fragment if present.
- The {{FragmentResultOptions/blockSize}} of the fragment.
15. Return |fragment|.