>
function). It consists of:
- input properties which is a list of
DOMStrings
- child input properties which is a list of
DOMStrings
.
- child display a {{ChildDisplayType}}.
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}
------------------------------------------
[Exposed=LayoutWorklet]
enum ChildDisplayType {
"block",
"normal",
};
Issue: "normal" is a bad name?
The document has a map of document layout definitions. Initially this map
is empty; it is populated when {{registerLayout(name, layoutCtor)}} is called.
The {{LayoutWorkletGlobalScope}} has a map of layout definitions. Initially this
map is empty; it is populated when {{registerLayout(name, layoutCtor)}} is called.
The {{LayoutWorkletGlobalScope}} has a map of layout class instances. Initially
this map is empty; it is populated when the user agent calls either determine the intrinsic
sizes or generate a fragment for a box.
When the
registerLayout(|name|, |layoutCtor|) method
is called, the user agent
must run the following steps:
1. If the |name| is an empty string,
throw a
TypeError and abort all these steps.
2. Let |layoutDefinitionMap| be {{LayoutWorkletGlobalScope}}'s
layout definitions map.
3. If |layoutDefinitionMap|[|name|]
exists throw a
"
InvalidModificationError"
DOMException and abort all these steps.
4. Let |inputProperties| be an empty
sequence<DOMString>
.
5. Let |inputPropertiesIterable| be the result of
Get(|layoutCtor|, "inputProperties").
6. If |inputPropertiesIterable| is not undefined, then set |inputProperties| to the result of
converting |inputPropertiesIterable| to a
sequence<DOMString>
. If an
exception is
thrown, rethrow the exception and abort all these steps.
Note: The list of CSS properties provided by the input properties getter can either be
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 contains currently invalid properties for the user agent. For example
margin-bikeshed-property
.
7. Let |childInputProperties| be an empty
sequence<DOMString>
.
8. Let |childInputPropertiesIterable| be the result of
Get(|layoutCtor|,
"childInputProperties").
9. If |childInputPropertiesIterable| is not undefined, then set |childInputProperties| to the
result of
converting |childInputPropertiesIterable| to a
sequence<DOMString>
. If an exception is
thrown, rethrow the exception
and abort all these steps.
10. Let |childDisplay| be a {{ChildDisplayType}} set to
"block"
.
11. Let |childDisplayValue| be the result of
Get(|layoutCtor|, "childDisplay").
12. If |childDisplayValue| if not undefined, then then |childDisplay| to the result of
converting |childDisplayValue| to a {{ChildDisplayType}}. If an exception is
thrown, rethrow the exception and abort all these steps.
13. If the result of
IsConstructor(|layoutCtor|) is false,
throw a
TypeError and abort all these steps.
14. Let |prototype| be the result of
Get(|layoutCtor|, "prototype").
15. If the result of
Type(|prototype|) is not Object,
throw a
TypeError and
abort all these steps.
16. Let |layout| be the result of
Get(|prototype|,
"layout"
).
17. If the result of
IsCallable(|layout|) is false,
throw a
TypeError and
abort all these steps.
18. If |layout|'s
\[[FunctionKind]]
internal slot is not
"generator"
,
throw a
TypeError and abort all these steps.
19. Let |intrinsizeSizes| be the result of
Get(|prototype|,
"intrinsicSizes"
).
20. If the result of
IsCallable(|intrinsizeSizes|) is false,
throw a
TypeError and abort all these steps.
21. If |intrinsizeSizes|'s
\[[FunctionKind]]
internal slot is not
"generator"
,
throw a
TypeError and abort all these steps.
22. Let |definition| be a new
layout definition with:
-
class constructor being |layoutCtor|.
-
layout generator function being |layout|.
-
intrinsic sizes generator function being |intrinsizeSizes|.
-
constructor valid flag being
true.
-
input properties being |inputProperties|.
-
child input properties being |childInputProperties|.
-
child display being |childDisplay|.
23.
Set |layoutDefinitionMap|[|name|] to |definition|.
24.
Queue a task to run the following steps:
1. Let |documentLayoutDefinitionMap| be the associated
document's document layout
definitions map.
2. Let |documentDefinition| be a new
document layout definition with:
-
input properties being |inputProperties|.
-
child input properties being
|childInputProperties|.
-
child display being |childDisplay|.
3. If |documentLayoutDefinitionMap|[|name|]
exists, run the following steps:
1. Let |existingDocumentDefinition| be the result of
get
|documentLayoutDefinitionMap|[|name|].
2. If |existingDocumentDefinition| is
"invalid"
, abort all these steps.
3. If |existingDocumentDefinition| and |documentDefinition| are not equivalent, (that is
input properties,
child input properties, and
child display are different), then:
Set |documentLayoutDefinitionMap|[|name|] to
"invalid"
.
Log an error to the debugging console stating that the same class was registered
with different
inputProperties
,
childInputProperties
, or
childDisplay
.
4. Otherwise,
set |documentLayoutDefinitionMap|[|name|] to
|documentDefinition|.
Note: The shape of the class should be:
class MyLayout {
static get inputProperties() { return ['--foo']; }
static get childrenInputProperties() { return ['--bar']; }
static get childDisplay() { return 'normal'; }
*intrinsicSizes(children, styleMap) {
// Intrinsic sizes code goes here.
}
*layout(constraintSpace, children, styleMap, edges, 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.
};
[Exposed=LayoutWorklet]
interface IntrinsicSizesRequest {
// Has internal slots:
// [[layoutChild]] - The layout child to calculate the intrinsic sizes for.
};
The layout method and intrinsic sizes 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).
The same applies for the {{LayoutChild/intrinsicSizes()}} method.
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.layoutChild;
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.
}
}
Performing Layout {#performing-layout}
--------------------------------------
// This is the final return value from the author defined layout() method.
dictionary FragmentResultOptions {
double inlineSize = 0;
double blockSize = 0;
sequence<Fragment> childFragments = [];
BreakTokenOptions breakToken = null;
double alignmentBaseline = null;
};
dictionary IntrinsicSizesResultOptions {
double maxContentSize;
double minContentSize;
};
interface IntrinsicSizes {
readonly attribute double minContentSize;
readonly attribute double maxContentSize;
};
Issue: Need to specify that the {{LayoutChild}} objects should remain the same between layouts so
the author can store information? Not sure.
### Determining Intrinsic Sizes ### {#determining-intrinsic-sizes}
The determine the intrinsic sizes algorithm defines how a user agent is to query the author
defined layout for a box's intrinsic sizes information.
Note: The determine the intrinsic sizes algorithm allows for user agents to cache an arbitary
number of previous invocations to reuse.
When the user agent wants to
determine the intrinsic sizes of a
layout API formatting
context for a given |box|, |childBoxes| it
must run the following steps:
1. Let |layoutFunction| be the <
> for the computed value of <>
for |box|.
2. If the intrinsic sizes valid flag for the |layoutFunction| is
intrinsic-sizes-valid the user agent may use the intrinsic sizes from the
previous invocation. If so it may abort all these steps and use the previous value
for the intrinsic sizes.
3. Set the intrinsic sizes valid flag for the |layoutFunction| to
intrinsic-sizes-valid.
4. Let |name| be the first argument of the |layoutFunction|.
5. Let |documentDefinition| be the result of get a document layout definition.
If get a document layout definition returned failure, or if |documentDefinition| is
"invalid"
, then let |box| fallback to the flow layout and abort all
these steps.
6. Let |workletGlobalScope| be a {{LayoutWorkletGlobalScope}} from the list of worklet's
WorkletGlobalScopes from the layout {{Worklet}}.
The user agent must have, and select from at least two
{{LayoutWorkletGlobalScope}}s in the worklet's WorkletGlobalScopes list,
unless the user agent is under memory constraints.
Note: This is to ensure that authers do not rely on being able to store state on the global
object or non-regeneratable state on the class.
The user agent may also create a WorkletGlobalScope at this time, given the
layout {{Worklet}}.
7. Run invoke a intrinsic sizes callback given |name|, |box|, |childBoxes|, and
|workletGlobalScope| optionally in parallel.
Note: If the user agent runs invoke a intrinsic sizes callback on a thread in
parallel, it should select a layout worklet global scope which can be used on that
thread.
When the user agent wants to
invoke a intrinsic sizes callback given |name|, |box|,
|childBoxes|, and |workletGlobalScope|, it
must run the following steps:
1. Let |definition| be the result of
get a layout definition given |name|, and
|workletGlobalScope|.
If
get a layout definition returned failure, let the |box| fallback to the
flow
layout and abort all these steps.
2. Let |layoutInstance| be the result of
get a layout class instance given |name|, |box|,
|definition|, |workletGlobalScope|.
If
get a layout class instance returned failure, let the |box| fallback to the
flow layout and abort all these steps.
3. Let |inputProperties| be |definition|'s
input properties.
4. Let |childInputProperties| be |definition|'s
child input
properties.
5. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with
only the
computed value's for properties listed in |inputProperties| for |box|.
6. Let |children| be a new
list populated with new {{LayoutChild}} objects which
represent |childBoxes|.
The {{LayoutChild/styleMap}} on each {{LayoutChild}} should be a new
{{StylePropertyMapReadOnly}} populated with
only the
computed value's for
properties listed in |childInputProperties|.
7. At this stage the user agent may re-use the
intrinsic sizes from a previous invocation
if |children|, |styleMap| are equivalent to that previous invocation. If so let the
intrinsic sizes the cached intrinsic sizes and abort all these steps.
8. Let |intrinsicSizesGeneratorFunction| be |definition|'s
intrinsic sizes generator
function.
9. Let |intrinsicSizesGenerator| be the result of
Invoke(|intrinsicSizesGeneratorFunction|, |layoutInstance|, «|styleMap|,
|children|»).
If an exception is
thrown the let |box| fallback to the
flow layout and abort
all these steps.
10. Let |intrinsicSizesValue| be the result of
run a generator given
|intrinsicSizesGenerator|, and
"intrinsic-sizes"
.
If
run a generator returned failure, then let |box| fallback to the
flow
layout and abort all these steps.
11. Let |intrinsicSizes| be the result of
converting |intrinsicSizesValue| to a
{{IntrinsicSizesResultOptions}}. If an exception is
thrown, let |box| fallback to the
flow layout and abort all these steps.
12. Set the
intrinsic sizes of |box|:
- Let |intrinsicSizes|'s {{IntrinsicSizesResultOptions/minContentSize}} be the
min-content size of |box|.
- Let |intrinsicSizes|'s {{IntrinsicSizesResultOptions/maxContentSize}} be the
max-content size of |box|.
### Generating Fragments ### {#generating-fragments}
The generate a fragment algorithm defines how a user agent is to generate a box's
fragment for an author defined layout.
Note: The generate a fragment algorithm allows for user agents to cache an arbitary number of
previous invocations to reuse.
When the user agent wants to
generate a fragment of a
layout API formatting
context for a given |box|, |childBoxes|, |internalConstraintSpace|, and an optional
|internalBreakToken| it
must run the following steps:
1. Let |layoutFunction| be the <
> for the computed value of <>
for |box|.
2. If the layout valid flag for the |layoutFunction| is layout-valid the user
agent may use the intrinsic sizes from the previous invocation. If so it
may abort all these steps and use the previous value for the intrinsic sizes.
3. Set the layout valid flag for the |layoutFunction| to layout-valid.
4. Let |name| be the first argument of the |layoutFunction|.
5. Let |documentDefinition| be the result of get a document layout definition.
If get a document layout definition returned failure, or if |documentDefinition| is
"invalid"
, then let |box| fallback to the flow layout and abort all
these steps.
6. Let |workletGlobalScope| be a {{LayoutWorkletGlobalScope}} from the list of worklet's
WorkletGlobalScopes from the layout {{Worklet}}.
The user agent must have, and select from at least two
{{LayoutWorkletGlobalScope}}s in the worklet's WorkletGlobalScopes list,
unless the user agent is under memory constraints.
Note: This is to ensure that authers do not rely on being able to store state on the global
object or non-regeneratable state on the class.
The user agent may also create a WorkletGlobalScope at this time, given the
layout {{Worklet}}.
7. Run invoke a layout callback given |name|, |box|, |childBoxes|,
|internalConstraintSpace|, |internalBreakToken|, and |workletGlobalScope| optionally in
parallel.
Note: If the user agent runs invoke a intrinsic sizes callback on a thread in
parallel, it should select a layout worklet global scope which can be used on that
thread.
When the user agent wants to
invoke a layout callback given |name|, |box|, |childBoxes|,
|internalConstraintSpace|, |internalBreakToken|, and |workletGlobalScope|, it
must run the
following steps:
1. Let |definition| be the result of
get a layout definition given |name|, and
|workletGlobalScope|.
If
get a layout definition returned failure, let the |box| fallback to the
flow
layout and abort all these steps.
2. Let |layoutInstance| be the result of
get a layout class instance given |name|, |box|,
|definition|, |workletGlobalScope|.
If
get a layout class instance returned failure, let the |box| fallback to the
flow layout and abort all these steps.
3. Let |inputProperties| be |definition|'s
input properties.
4. Let |childInputProperties| be |definition|'s
child input
properties.
5. Let |styleMap| be a new {{StylePropertyMapReadOnly}} populated with
only the
computed value's for properties listed in |inputProperties| for |box|.
6. Let |children| be a new
list populated with new {{LayoutChild}} objects which
represent |childBoxes|.
The {{LayoutChild/styleMap}} on each {{LayoutChild}} should be a new
{{StylePropertyMapReadOnly}} populated with
only the
computed value's for
properties listed in |childInputProperties|.
7. Let |constraintSpace| be a new {{ConstraintSpace}} populated with the appropriate information
from |internalConstraintSpace|.
8. Let |edges| be a new {{LayoutEdgeSizes}} populated with the
computed value for all the
box model edges for |box|.
9. Let |breakToken| be a new {{BreakToken}} populated with the appropriate information from
|internalBreakToken|.
If |internalBreakToken| is null, let |breakToken| be null.
10. At this stage the user agent may re-use a
fragment from a previous invocation if
|children|, |styleMap|, |constraintSpace|, |breakToken| are equivalent to that previous
invocation. If so let the fragment output be that cached fragment and abort all these steps.
11. Let |layoutGeneratorFunction| be |definition|'s
layout generator function.
12. Let |layoutGenerator| be the result of
Invoke(|layoutGeneratorFunction|,
|layoutInstance|, «|styleMap|, |children|, |constraintSpace|, |edges|, |breakToken|»).
If an exception is
thrown the let |box| fallback to the
flow layout and abort
all these steps.
13. Let |fragmentValue| be the result of
run a generator given |layoutGenerator|, and
"layout"
.
If
run a generator returned failure, then let |box| fallback to the
flow
layout and abort all these steps.
14. Let |fragment| be the result of
converting |fragmentValue| to a
{{FragmentResultOptions}}. If an exception is
thrown, let |box| fallback to the
flow layout and abort all these steps.
15. Create a
fragment for |box| with:
- The
inline size set to |fragment|'s {{FragmentResultOptions/inlineSize}}.
- The
block size set to |fragment|'s {{FragmentResultOptions/blockSize}}.
- The child fragments set to |fragment|'s {{FragmentResultOptions/childFragments}}
list. The ordering
is important as this 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 information set to |fragment|'s
{{FragmentResultOptions/breakToken}}.
TODO: storage of the break token.
- TODO: baselines.
### Utility Algorithms ### {#utility-algorithms}
The section specifies algorithms common to the determine the intrinsic sizes and generate
a fragment algorithms.
When the user agent wants to
get a document layout definition given |name|, it
must run the following steps:
1. Let |documentLayoutDefinitionMap| be the associated
document's document layout
definitions map.
2. If |documentLayoutDefinitionMap|[|name|] does not
exist, return failure and
abort all these steps.
3. Return the result of
get |documentLayoutDefinitionMap|[|name|].
When the user agent wants to
get a layout definition given |name|, and
|workletGlobalScope|, it
must run the following steps:
1. Let |layoutDefinitionMap| be |workletGlobalScope|'s
layout definitions map.
2. If |layoutDefinitionMap|[|name|] does not
exist, run the following steps:
1.
Queue a task to run the following steps:
1. Let |documentLayoutDefinitionMap| be the associated
document's
document
layout definition map.
2.
Set |documentLayoutDefinitionMap|[|name|] to
"invalid"
.
3. The user agent
should log an error to the debugging console stating that a
class wasn't registered in all {{LayoutWorkletGlobalScope}}s.
2. Return failure, and abort all these steps.
3. Return the result of
get |layoutDefinitionMap|[|name|].
When the user agent wants to
get a layout class instance given |name|, |box|,
|definition|, and |workletGlobalScope|, it
must run the following steps:
1. Let |layoutClassInstanceMap| be |workletGlobalScope|'s
layout class instances map.
2. Let |key| be a stable
key which is unique to |box| and |name|.
3. Let |layoutInstance| be the result of
get |layoutClassInstanceMap|[|key|]. If
|layoutInstance| is null, run the following steps:
1. If the
constructor valid flag on |definition| is false, let |box| fallback to the
flow layout and a abort all these steps.
2. Let |layoutCtor| be the
class constructor on |definition|.
3. Let |layoutInstance| be the result of
Construct(|layoutCtor|).
If
construct throws an exception, set the |definition|'s
constructor valid
flag to false, then return failure and abort all these steps.
4.
Set |layoutClassInstanceMap|[|key|] to |layoutInstance|.
4. Return |layoutInstance|.
When the user agent wants to
run a generator given |generator|, and |generatorType|, it
must run the following steps:
1. Let |nextResult| be the result of calling
Invoke(
next
, |generator|).
If an exception is
thrown return failure, and abort all these steps.
2. Perform the following substeps until the result of
Get(|nextResult|,
"done"
) is
true
.
1. Let |requests| be the result of
Get(|nextResult|,
"value"
).
2. Let |results| be an
empty list.
3.
For each |request| in |requests| perform the following substeps:
1. Let |result| be null.
2. If |request| is a {{IntrinsicSizesRequest}} then:
1. Let |layoutChild| be the result of looking up the internal slot
\[[layoutChild]]
on |request|.
2. Let |result| be a new {{IntrinsicSizes}} with:
- {{IntrinsicSizes/minContentSize}} being |layoutChild|'s
min-content
size.
- {{IntrinsicSizes/maxContentSize}} being |layoutChild|'s
max-content
size.
3. If |request| is a {{FragmentRequest}} and |generatorType| is
"layout"
then:
1. Let |layoutChild| be result of looking up the internal slot
\[[layoutChild]]
on |request|.
2. Let |childConstraintSpace| be the result of looking up the internal slot
\[[childConstraintSpace]]
on |request|.
3. Let |childBreakToken| be the result of looking up the internal slot
\[[childBreakToken]]
on |request|.
4. Let |internalFragment| be the result of the user agent producing a
fragment based on |layoutChild|, |childConstraintSpace|, and
|childBreakToken|.
5. Let |result| be a new {{Fragment}} with:
- {{Fragment/inlineSize}} being |internalFragment|'s
inline size.
- {{Fragment/blockSize}} being |internalFragment|'s
block size.
- {{Fragment/inlineOffset}} initially set to 0.
- {{Fragment/blockOffset}} initially set to 0.
- {{Fragment/breakToken}} being a new {{ChildBreakToken}} representing
|layoutChild|'s internal break token.
- {{Fragment/alignmentBaseline}} being |internalFragment|'s
alignment
baseline if |childConstraintSpace| had
{{ConstraintSpaceOptions/alignmentBaseline}} set to
true, otherwise
just null.
4. If |result| is null (that is neither of the above branches was taken), return
failure, and abort all these steps.
5.
Append |result| to |results|.
The user agent may perform the above loop out of order, and
in parallel. The ordering
for |requests| and |results| however
must be consistent.
Note: This is to allow user agents to run the appropriate layout algorithm on a different
thread, or asynchronously (e.g. time slicing layout work with other work). If the user
agent performs the loop in parallel, the outside loop has to wait until all the cross
thread tasks are complete before calling the generator again. It cannot return partial
results to the author.
4. Let |nextResult| be the result of calling
Invoke(
next
,
|generator|, |results|).
If an exception is
thrown then return failure, and abort all these steps.
3. Return the result of calling
Get(|nextResult|,
"value"
).