CSS 布局 API 第 1 级

W3C 首次公开工作草案,

此版本:
https://www.w3.org/TR/2018/WD-css-layout-api-1-20180412/
最新发布版本:
https://www.w3.org/TR/css-layout-api-1/
编辑草案:
https://drafts.css-houdini.org/css-layout-api-1/
反馈:
public-houdini@w3.org 邮件主题为 “[css-layout-api] … 信息主题 …” (存档)
问题跟踪:
GitHub
规范内嵌问题
编辑:
(微软)
(谷歌)
Tab Atkins-Bittner (谷歌)
(谷歌)
(微软)

摘要

本规范描述了一种 API,允许开发人员响应计算样式和 盒模型 以及 盒树 的变化来布局盒子。

本文档状态

本节描述了本文档在发布时的状态。其他文档可能会取代本文档。当前 W3C 发布的文档列表及该技术报告的最新修订版可在 W3C 技术报告索引 https://www.w3.org/TR/ 中找到。

本文档为 首次公开工作草案。

作为首次公开工作草案发布并不意味着 W3C 会员对其表示认可。这是一份草案文件,可能会随时更新、替换或废止。引用此文档不应被视为引用最终的工作成果。

建议通过 GitHub Issues 讨论本规范。 提交问题时,请在标题中包含 “css-layout-api”,最好像这样: “[css-layout-api] …评论摘要…”。 所有问题和评论均 存档。

本文档由 CSS 工作组 制作。

本文档由依据 W3C 专利政策 运营的一个小组制作。W3C 维护了与该小组交付成果相关的任何专利披露的 公开列表;该页面还包括披露专利的说明。如果个人掌握了他们认为包含 必要声明 的专利,则必须根据 W3C 专利政策第 6 条 披露相关信息。

本文档受 2018 年 2 月 1 日 W3C 流程文件 约束。

1. 介绍

本节非规范性内容。

CSS 的布局阶段负责从片段和盒树中生成和定位内容。

本规范描述了一种 API,允许开发人员根据计算的样式和盒树的变化来布局一个盒模型。

2. 布局 API 容器

向 可选项值中新增了一个值, 该值被添加到 <display-inside> 产生式中: layout(<ident>)。

layout()
此值会使元素生成一个布局 API 容器盒。

布局 API 容器是由 <display-inside> 计算值为 layout() 的元素所生成的盒。

布局 API 容器为其内容建立一个新的布局 API 格式化上下文。这与建立块格式化上下文相同,只不过使用 作者提供的布局来代替块布局。 例如,浮动不会侵入布局 API 容器,并且布局 API 容器的 外边距不会与其内容的外边距折叠。

布局 API 容器的所有流内子项都称为布局 API 子项,并且 使用作者定义的布局进行布局。

布局 API 容器为其内容形成包含块,与块 容器完全相同。[CSS21]

注:在该规范的未来级别中,可能会提供一种方式来 覆盖包含块 行为。

overflow 属性适用于布局 API 容器。这将在 §4.3 溢出中讨论。

由于布局完全由作者决定,因此其他布局模式(例如 flex、block)中使用的属性可能并不适用。例如,作者可能不会遵循子项上的 margin 属性。

2.1. 布局 API 容器绘制

布局 API 容器的子元素绘制方式与内联块[CSS21]完全相同,除了布局方法返回的顺序(通过childFragments) 替代了原始文档顺序,且z-index 的非auto值即使position是static时,也会创建一个层叠上下文。

2.2. 盒树转换

布局 API 子元素可以根据布局选项的值以不同方式表现,childDisplay 由类上的layoutOptions设置。

如果布局选项的childDisplay 的值为"block",则该子元素的display值将被块化。这类似于弹性容器或网格容器的子元素。 参见[css3-display]。

如果布局选项的childDisplay 的值为"normal",则不会发生块化。 相反,具有<display-outside>计算值为内联的子元素(根内联盒) 在调用layoutNextFragment() 时,将为每一行生成一个布局片段。

注意: 这允许作者调整每一行的可用内联大小,并单独定位每一行。

表示根内联盒的布局子元素的子元素也有一些额外的转换。

在上述两种情况下,子元素都会变成原子内联。

注意: 当用户代理遇到块级盒子时,不会执行任何“内联拆分”或分片。

注意:在以下示例中,“inline-span”将表示为单个布局子元素, 同时“block”和“float”将成为原子内联。
<span id="inline-span">
  Text
  <div id="block"></div>
  <div id="float"></div>
  Text
</span>

3. 布局 API 模型和术语

本节概述了提供给作者的布局 API。

当前布局是我们正在为其执行布局的盒模型的布局算法。

父布局是盒模型的直接父元素的布局算法(该布局算法正在请求执行当前布局)。

子布局是LayoutChild的布局算法,属于当前布局的子元素。

3.1. 布局子元素

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

    IntrinsicSizesRequest intrinsicSizes();
    LayoutFragmentRequest layoutNextFragment(LayoutConstraints constraints, ChildBreakToken breakToken);
};

LayoutChild包含以下内部槽位:


LayoutChild表示一个 CSS 生成的盒模型,在布局发生之前(这些盒模型的计算值都不是display为none)。

LayoutChild本身不包含任何布局信息(如内联或块大小),但可以用来生成包含布局信息的LayoutFragment。

作者不能通过此 API 构造LayoutChild,这是在渲染引擎的独立阶段(样式解析后)完成的。

LayoutChild可以由以下元素生成:

注意:以下示例将被放置到三个LayoutChild中:
<style>
  #box::before { content: 'hello!'; }
</style>
<div id="box">A block level box with text.</div>
<img src="..." />
注意:以下示例将被放置到一个LayoutChild中,因为它们共享一个根内联盒:
This is a next node, <span>with some additional styling,
that may</span> break over<br>multiple lines.

多个非原子内联的内容将被放置在同一个LayoutChild中,以允许渲染引擎跨元素边界执行文本排版。

注意:以下示例应生成一个布局片段,但它来自三个非原子内联元素:
ع<span style="color: blue">ع</span>ع

一个LayoutChild数组被传递到布局方法中,表示正在布局的当前盒子的子元素。

从 LayoutChild this 获取 styleMap 时, 用户代理必须执行以下步骤:
  1. 返回 this 的 StylePropertyMapReadOnly, 其包含在 [[styleMap]] 内部槽中。

当在 LayoutChild this 上调用 layoutNextFragment(constraints, breakToken) 方法时,用户代理必须执行以下步骤:
  1. 令 request 为一个新的 LayoutFragmentRequest, 并具有以下内部槽:

  2. 返回 request。

当在 LayoutChild this 上调用 intrinsicSizes() 方法时, 用户代理必须执行以下步骤:
  1. 令 request 为一个新的 IntrinsicSizesRequest, 并具有以下内部槽:

  2. 返回 request。

注:layoutNextFragment() 和 intrinsicSizes() 都不会 同步运行。完整说明请参见 §5.5.1 请求对象。

3.1.1. 布局子元素与盒树

每个盒模型都有一个[[layoutChildMap]]内部槽位,这是一个LayoutWorkletGlobalScope到LayoutChild的有序映射。

当用户代理在给定workletGlobalScope、name和box的情况下获取布局子元素时,必须执行以下步骤:
  1. 确认以下内容:

  2. 将layoutChildMap设置为box的[[layoutChildMap]]。

  3. 如果layoutChildMap[workletGlobalScope]不存在,执行以下步骤:

    1. 通过给定的name和workletGlobalScope,获取布局定义。

      确认获取布局定义成功,且definition不是"invalid"。

    2. 将childInputProperties设置为definition的子输入属性。

    3. 创建一个新的LayoutChild,其内部槽位为:

    4. 将layoutChildMap[workletGlobalScope]设置为layoutChild。

  4. 返回layoutChildMap[workletGlobalScope]的获取结果。

当一个盒模型插入到盒树中时,用户代理可以为所有LayoutWorkletGlobalScope预填充[[layoutChildMap]]。

当一个盒模型从盒树中移除时,用户代理必须清除[[layoutChildMap]]。

当用户代理想要在给定box的情况下更新布局子元素样式时,必须执行以下步骤:
  1. 确认以下内容:

    • box当前已附加到盒树中。

  2. 如果box的包含块不是布局 API 容器,则中止这些步骤。

  3. 将layoutChildMap设置为box的[[layoutChildMap]]。

  4. 对 layoutChildMap中的每个layoutChild进行以下操作:

    1. 将styleMap设置为layoutChild的[[styleMap]]。

    2. 根据box的新计算样式更新styleMap的声明。

当盒模型的计算样式发生变化时,用户代理必须运行更新布局子元素样式算法。

3.2. 布局片段

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

    attribute double inlineOffset;
    attribute double blockOffset;

    readonly attribute any data;

    readonly attribute ChildBreakToken? breakToken;
};

LayoutFragment具有以下内部槽位:


LayoutFragment表示一个 CSS 片段,该片段是在LayoutChild上执行布局后生成的。这是由layoutNextFragment()方法生成的。

LayoutFragment具有inlineSize和blockSize属性,这些属性由相应子元素的布局算法设置。它们表示该 CSS 片段的边框盒大小,且相对于当前布局的书写模式。

inlineSize和blockSize属性不可更改。如果当前布局需要不同的inlineSize或blockSize,则作者必须再次执行layoutNextFragment(),并使用不同的参数来获得不同的结果。

当前布局中的作者可以通过设置其inlineOffset和blockOffset属性来定位生成的LayoutFragment。如果作者未设置,它们默认值为零。inlineOffset和blockOffset属性表示LayoutFragment相对于其父元素边框盒的位置,未应用变换或定位(例如,如果片段是相对定位的情况)。

布局算法执行块状布局(按块方向顺序排列片段),同时将其子元素在内联方向上居中对齐。
registerLayout('block-like', class {
    *intrinsicSizes(children, edges, styleMap) {
      const childrenSizes = yield children.map((child) => {
          return child.intrinsicSizes();
      });

      const maxContentSize = childrenSizes.reduce((max, childSizes) => {
          return Math.max(max, childSizes.maxContentSize);
      }, 0) + edges.all.inline;

      const minContentSize = childrenSizes.reduce((max, childSizes) => {
          return Math.max(max, childSizes.minContentSize);
      }, 0) + edges.all.inline;

      return {maxContentSize, minContentSize};
    }

    *layout(children, edges, constraints, styleMap) {
        const availableInlineSize = constraints.fixedInlineSize - edges.all.inline;
        const availableBlockSize = (constraints.fixedBlockSize || Infinity) - edges.all.block;

        const childFragments = [];
        const childConstraints = { availableInlineSize, availableBlockSize };

        const childFragments = yield children.map((child) => {
            return child.layoutNextFragment(childConstraints);
        });

        let blockOffset = edges.all.blockStart;
        for (let fragment of childFragments) {
            // 按块状方式排列片段,并在内联方向居中。
            fragment.blockOffset = blockOffset;
            fragment.inlineOffset = Math.max(
                edges.all.inlineStart,
                (availableInlineSize - fragment.inlineSize) / 2);

            blockOffset += fragment.blockSize;
        }

        const autoBlockSize = blockOffset + edges.all.blockEnd;

        return {
            autoBlockSize,
            childFragments,
        };
    }
});

布局 API 容器可以通过data属性与其他布局 API 容器通信。该属性由data成员在FragmentResultOptions字典中设置。

LayoutFragment的breakToken指定了LayoutChild最后一次分割的位置。如果breakToken为null,则LayoutChild不会为该token链生成更多LayoutFragment。breakToken可以传递给layoutNextFragment()函数以为特定子元素生成下一个LayoutFragment。breakToken不可更改。如果当前布局需要不同的breakToken,作者必须再次执行layoutNextFragment(),并使用不同的参数。

当用户代理想要在给定generator、layoutFragmentRequest和internalFragment的情况下创建布局片段时,必须执行以下步骤:
  1. 将targetRealm设置为generator的域。

  2. 创建一个新的LayoutFragment,其包含:

  3. 返回fragment。

3.3. 固有尺寸

[Exposed=LayoutWorklet]
interface IntrinsicSizes {
  readonly attribute double minContentSize;
  readonly attribute double maxContentSize;
};

IntrinsicSizes对象具有以下内部槽位:


IntrinsicSizes对象表示一个 CSS 盒模型的最小内容尺寸和最大内容尺寸。它具有minContentSize和maxContentSize属性,这些属性表示当前布局中LayoutChild的边框盒最小/最大内容贡献。这些属性相对于当前布局的书写模式的内联方向。

minContentSize和maxContentSize属性不可更改。在当前布局过程中,LayoutChild的这些值不能更改。

下面的示例展示了两个子元素的边框盒固有尺寸。
<style>
.child-0 {
  width: 380px;
  border: solid 10px;
}

.child-1 {
  border: solid 5px;
}

.box {
  display: layout(intrinsic-sizes-example);
  font: 25px/1 Ahem;
}
</style>

<div class="box">
  <div class="child-0"></div>
  <div class="child-1">XXX XXXX</div>
</div>
registerLayout('intrinsic-sizes-example', class {
    *intrinsicSizes(children, edges, styleMap) {
      const childrenSizes = yield children.map((child) => {
          return child.intrinsicSizes();
      });

      childrenSizes[0].minContentSize; // 400, (380+10+10) child has a fixed size.
      childrenSizes[0].maxContentSize; // 400, (380+10+10) child has a fixed size.

      childrenSizes[1].minContentSize; // 100, size of "XXXX".
      childrenSizes[1].maxContentSize; // 200, size of "XXX XXXX".
    }

    *layout() {}
});
当用户代理想要在给定intrinsicSizesRequest和internalIntrinsicSizes的情况下创建固有尺寸对象时,必须执行以下步骤:
  1. 创建一个新的intrinsicSizes IntrinsicSizes对象,包含:

  2. 返回intrinsicSizes。

3.4. 布局约束

[Constructor(optional LayoutConstraintsOptions options),Exposed=LayoutWorklet]
interface LayoutConstraints {
    readonly attribute double availableInlineSize;
    readonly attribute double availableBlockSize;

    readonly attribute double? fixedInlineSize;
    readonly attribute double? fixedBlockSize;

    readonly attribute double percentageInlineSize;
    readonly attribute double percentageBlockSize;

    readonly attribute double? blockFragmentationOffset;
    readonly attribute BlockFragmentationType blockFragmentationType;

    readonly attribute any data;
};

dictionary LayoutConstraintsOptions {
    double availableInlineSize = 0;
    double availableBlockSize = 0;

    double fixedInlineSize;
    double fixedBlockSize;

    double percentageInlineSize;
    double percentageBlockSize;

    double blockFragmentationOffset;
    BlockFragmentationType blockFragmentationType = "none";

    any data;
};

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

LayoutConstraints对象被传递给布局方法,表示用于当前布局进行内部布局的所有约束条件。它还用于将可用空间的信息传递给子布局。

LayoutConstraints对象具有availableInlineSize和availableBlockSize属性。这表示布局中应遵守的可用空间,以生成LayoutFragment。

注意: 某些布局可能需要生成超出此尺寸的LayoutFragment。例如,一个替换元素。父布局应预期这种情况并适当地处理。

父布局可能要求当前布局必须具有特定的尺寸。如果指定了fixedInlineSize或fixedBlockSize,当前布局应生成具有指定方向上尺寸的LayoutFragment。

该布局算法在内联方向上执行类似于弹性盒子的空余空间分配。它创建了子布局约束,指定子元素应具有固定的内联尺寸。
registerLayout('flex-distribution-like', class {
    *intrinsicSizes(children, edges, styleMap) {
      const childrenSizes = yield children.map((child) => {
          return child.intrinsicSizes();
      });

      const maxContentSize = childrenSizes.reduce((sum, childSizes) => {
          return sum + childSizes.maxContentSize;
      }, 0) + edges.all.inline;

      const minContentSize = childrenSizes.reduce((max, childSizes) => {
          return sum + childSizes.minContentSize;
      }, 0) + edges.all.inline;

      return {maxContentSize, minContentSize};
    }

    *layout(children, edges, constraints, styleMap) {

        const availableInlineSize =
            constraints.fixedInlineSize - edges.all.inline;
        const availableBlockSize =
            (constraints.fixedInlineSize || Infinity) - edges.all.block;

        const childConstraints = { availableInlineSize, availableBlockSize };

        const unconstrainedChildFragments = yield children.map((child) => {
            return child.layoutNextFragment(childConstraints);
        });

        const unconstrainedSizes = [];
        const totalSize = unconstrainedChildFragments.reduce((sum, fragment, i) => {
            unconstrainedSizes[i] = fragment.inlineSize;
            return sum + fragment.inlineSize;
        }, 0);

        // 在子元素之间分配剩余空间。
        const remainingSpace = Math.max(0, inlineSize - totalSize);
        const extraSpace = remainingSpace / children.length;

        const childFragments = yield children.map((child, i) => {
            return child.layoutNextFragment({
                fixedInlineSize: unconstrainedSizes[i] + extraSpace,
                availableBlockSize
            });
        });

        // 位置片段。
        let inlineOffset = 0;
        let maxChildBlockSize = 0;
        for (let fragment of childFragments) {
            fragment.inlineOffset = inlineOffset;
            fragment.blockOffset = edges.all.blockStart;

            inlineOffset += fragment.inlineSize;
            maxChildBlockSize = Math.max(maxChildBlockSize, fragment.blockSize);
        }

        return {
            autoBlockSize: maxChildBlockSize + edges.all.block,
            childFragments,
        };
    }
});

LayoutConstraints对象具有percentageInlineSize和percentageBlockSize属性。这些属性表示在执行布局时百分比应解析的尺寸。

LayoutConstraints对象具有blockFragmentationType属性。当前布局应生成一个在blockFragmentationOffset处进行分片的LayoutFragment,如果可能的话。

当前布局可以根据blockFragmentationType选择不对某个LayoutChild进行分片,例如,如果该子元素具有类似于break-inside: avoid-page;的属性。

当用户代理想要在给定sizingMode、box和internalLayoutConstraints的情况下创建布局约束对象时,必须执行以下步骤:
  1. 如果sizingMode为"block-like",则:

    1. 将fixedInlineSize设置为计算box的边框盒内联尺寸的结果(相对于box的书写模式),就像块容器一样。

    2. 如果box的块尺寸是auto,则将fixedBlockSize设置为null,否则将其设置为计算box的边框盒块尺寸的结果,就像块容器一样。

    3. 返回一个新的LayoutConstraints对象,包含:

  2. 如果sizingMode为"manual",则:

    1. 返回一个新的LayoutConstraints对象,包含:

3.5. 分割与碎片化

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

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

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

enum BreakType { "none", "line", "column", "page", "region" };

LayoutChild可以生成多个LayoutFragment。 如果blockFragmentationType不为none,则LayoutChild可以在块方向上分片。此外,表示内联级内容的LayoutChild可能会逐行分片,如果布局选项的childDisplay(由layoutOptions设置)为"normal"。

可以使用前一个LayoutFragment的breakToken生成后续的LayoutFragment。 这告诉子布局从编码在ChildBreakToken中的点开始生成LayoutFragment。

此示例展示了一个简单的布局,它会在一定数量的 行中缩进子 片段。

此示例还演示了如何使用 LayoutFragment 的前一个 breakToken 来为 LayoutChild 生成下一个片段。

它还演示了如何使用 BreakToken 来遵循 LayoutConstraints 的 blockFragmentationType, 它从前一个 BreakToken 恢复布局。 它返回一个 FragmentResultOptions, 其中包含一个用于 恢复布局的 breakToken。

registerLayout('indent-lines', class {
    static layoutOptions = {childDisplay: 'normal'};
    static inputProperties = ['--indent', '--indent-lines'];

    *layout(children, edges, constraints, styleMap, breakToken) {

        // 确定我们的(内部)可用尺寸。
        const availableInlineSize =
            constraints.fixedInlineSize - edges.all.inline;
        const availableBlockSize =
            (constraints.fixedBlockSize || Infinity) - edges.all.block;

        // 确定要缩进的行数以及缩进量。
        const indent = resolveLength(constraints, styleMap.get('--indent'));
        let lines = styleMap.get('--indent-lines').value;

        const childFragments = [];

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

            // 移除所有我们已经为其生成片段的子项。
            children.splice(0, children.indexOf(childBreakToken.child));
        }

        let blockOffset = edges.all.blockStart;
        let child = children.shift();
        while (child) {
            const shouldIndent = lines-- > 0;

            // 根据缩进调整行内尺寸。
            const childAvailableInlineSize = shouldIndent ?
                availableInlineSize - indent : availableInlineSize;

            const childConstraints = {
                availableInlineSize: childAvailableInlineSize,
                availableBlockSize,
                percentageInlineSize: availableInlineSize,
                blockFragmentationType: constraints.blockFragmentationType,
            };

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

            // 定位片段。
            fragment.inlineOffset = shouldIndent ?
                edges.all.inlineStart + indent : edges.all.inlineStart;
            fragment.blockOffset = blockOffset;
            blockOffset += fragment.blockSize;

            // 检查是否超出了块分片限制。
            if (constraints.blockFragmentationType != 'none' &&
                blockOffset > constraints.blockSize) {
                break;
            }

            if (fragment.breakToken) {
                childBreakToken = fragment.breakToken;
            } else {
                // 如果片段没有断点令牌,我们将继续处理
                // 下一个子项。
                child = children.shift();
                childBreakToken = null;
            }
        }

        const autoBlockSize = blockOffset + edges.all.blockEnd;

        // 返回我们的片段。
        const result = {
            autoBlockSize,
            childFragments: childFragments,
        }

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

        return result;
    }
});

3.6. 边缘

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

  readonly attribute double blockStart;
  readonly attribute double blockEnd;

  // Convenience attributes for the sum in one direction.
  readonly attribute double inline;
  readonly attribute double block;
};

[Exposed=LayoutWorklet]
interface LayoutEdges {
  readonly attribute LayoutEdgeSizes border;
  readonly attribute LayoutEdgeSizes scrollbar;
  readonly attribute LayoutEdgeSizes padding;

  readonly attribute LayoutEdgeSizes all;
};

一个 LayoutEdges 对象被传入布局方法。它表示正在布局的当前框的 盒模型边缘 的大小。

这个 LayoutEdges 对象有 border、 scrollbar 和 padding 属性。每个属性都表示其相应边缘的宽度。

这个 LayoutEdges 对象有 all 属性。它是一个便捷属性,表示 border、scrollbar 和 padding 边缘的总和。

这个 LayoutEdgeSizes 对象表示每个 抽象方向(inlineStart、inlineEnd、blockStart、blockEnd)中边缘的宽度,以CSS像素为单位。

这个 inline 和 block 是 LayoutEdgeSizes 对象上的便捷属性,表示该方向的总和。

此示例展示了一个通过CSS样式化的节点,以及它的 LayoutEdges 可能包含的内容。
<style>
.container {
  width: 50px;
  height: 50px;
}

.box {
  display: layout(box-edges);

  padding: 10%;
  border: solid 2px;
  overflow-y: scroll;
}
</style>

<div class="container">
  <div class="box"></div>
</div>
registerLayout('box-edges', class {
    *layout(children, edges, constraints, styleMap, breakToken) {
        edges.padding.inlineStart; // 5 (as 10% * 50px = 5px).
        edges.border.blockEnd; // 2
        edges.scrollbar.inlineEnd; // UA-dependent, may be 0 or >0 (e.g. 16).
        edges.all.block; // 14 (2 + 5 + 5 + 2).
    }
}

4. 与其他模块的交互

本节描述了其他CSS模块如何与CSS布局API进行交互。

4.1. 大小调整

用户代理必须使用 LayoutConstraints 对象与 当前布局 进行通信,以指示他们希望片段的大小。

如果用户代理希望强制对框设置大小,它可以使用 fixedInlineSize 和 fixedBlockSize 属性来实现。

根据 布局选项 的 sizing(由类中的 layoutOptions 设置)的值,布局API容器可以通过不同方式接收大小信息。

如果 布局选项 的 sizing 值为 "block-like",那么传递给 布局API容器 的 LayoutConstraints:

如果 布局选项 的 sizing 值为 "manual",则用户代理在布局API容器的大小由所参与的格式化上下文强制设定时,除非提前强制设置特定的大小,否则不应预先计算 fixedInlineSize 和/或 fixedBlockSize,例如:

注意:在下面的示例中,布局API容器 的行内尺寸设置为50。
<style>
  #container {
    width: 100px;
    height: 100px;
    box-sizing: border-box;
    padding: 5px;
  }
  #layout-api {
    display: layout(foo);
    margin: 0 20px;
  }
</style>
<div id="container">
  <div id="layout-api"></div>
</div>

4.1.1. 定位布局大小调整

如果 布局API容器 是脱离文档流定位的,用户代理 必须 解算定位尺寸方程(CSS定位布局3 §8.1 绝对或固定定位、非替换元素的宽度,CSS定位布局3 §8.3 绝对或固定定位、非替换元素的高度),并设置合适的 fixedInlineSize 和 fixedBlockSize。

注意:在下面的示例中,布局API容器 的行内和块大小被固定为80。
<style>
  #container {
    position: relative;
    width: 100px;
    height: 100px;
  }
  #layout-api {
    display: layout(foo);
    top: 10px;
    bottom: 10px;
    left: 10px;
    right: 10px;
    position: absolute;
  }
</style>
<div id="container">
  <div id="layout-api"></div>
</div>

4.2. 定位

在该规范的这一层级中,所有定位均由用户代理处理。

因此:

注:在下面的示例中:
<style>
  #container {
    display: layout(foo);
    position: relative; /* container 是包含块 */
    width: 100px;
    height: 100px;
  }
  #child-relative {
    position: relative;
    left: 5px;
    top: 10px;
  }
</style>
<div id="container">
  <div id="child-relative"></div>
  <div id="child-absolute"></div>
</div>

4.3. 溢出

可滚动溢出对于 布局API容器 由用户代理在本规范级别处理。

布局API容器应像块容器那样计算其可滚动溢出。

即使作者的 布局API容器 将片段放置在 可滚动溢出区域内,相对定位或变换可能会导致片段移动,使得它的 可滚动溢出区域不会产生溢出。

4.4. 分片

父布局可以通过设置 blockFragmentationType 和 blockFragmentationOffset 来要求 当前布局进行分片。

例如,[css-multicol-1] 布局会将 blockFragmentationType 设置为 "column" 并将 blockFragmentationOffset 设置为需要子片段的地方。

4.5. 对齐

布局API容器 的第一/最后基线集与块容器生成的方式相同(参见 CSS 盒对齐 3 §9.1 确定盒子的基线)。不过,流内子元素的顺序应由布局方法返回的顺序决定(通过 childFragments)而不是文档顺序。

注意:在未来的规范级别中,作者将能够自行定义基线。这将以如下形式出现:

从 LayoutChild 中查询基线信息。

const fragment = yield child.layoutNextFragment({
  fixedInlineSize: availableInlineSize,
  baselineRequests: ['alphabetic', 'middle'],
});
fragment.baselines.get('alphabetic') === 25 /* 或其他值 */;

为 父布局 生成基线信息:

registerLayout('baseline-producing', class {
  *layout(children, edges, constraints, styleMap) {
    const result = {baselines: {}};

    for (let baselineRequest of constraints.baselineRequests) {
      // baselineRequest === 'alphabetic' 或其他值。
      result.baselines[baselineRequest] = 25;
    }

    return result;
  }
});

5. 布局

本节描述了CSS布局API如何与用户代理的布局引擎交互。

5.1. 概念

布局定义是一个结构体,描述了 LayoutWorkletGlobalScope 需要的关于作者定义布局的信息(可由layout()函数引用)。它包括:

文档布局定义是一个结构体,描述了文档需要的关于作者定义布局的信息(可由layout()函数引用)。它包括:

5.2. 布局失效

每个盒子都有一个关联的布局有效标志。它可以是布局有效或布局无效。初始状态为布局无效。

每个盒子都有一个关联的固有尺寸有效标志。它可以是固有尺寸有效或固有尺寸无效。初始状态为固有尺寸无效。

当用户代理想要使布局函数失效,并给定box时,用户代理必须执行以下步骤:
  1. 将layoutFunction设为box的layout()函数。如果它是其他类型的值(如grid),则终止这些步骤。

  2. 将name设为layoutFunction的第一个参数。

  3. 将documentDefinition设为获取文档布局定义的结果,给定name。

    如果获取文档布局定义失败,或documentDefinition为"无效",则终止这些步骤。

  4. 将inputProperties设为documentDefinition的输入属性。

  5. 将childInputProperties设为documentDefinition的子输入属性。

  6. 对于inputProperties中的每个property,如果property的计算值已更改,将该布局有效标志设为布局无效,并将固有尺寸有效标志设为固有尺寸无效。

  7. 对于childInputProperties中的每个property,如果property的计算值已更改,将该布局有效标志设为布局无效,并将固有尺寸有效标志设为固有尺寸无效。

当用户代理重新计算盒子的计算样式时,或该盒子的子项计算样式重新计算时,使布局函数失效 必须被运行。

当由LayoutChild表示的子盒子被添加或移除到盒子树中,或者其布局因计算样式更改或子项更改而失效时,并且这种失效需要向上传播到盒子树,将当前布局有效标志设为布局无效,并将当前固有尺寸有效标志设为固有尺寸无效。

当布局API容器的计算样式更改,并且此更改影响了LayoutEdges对象内的值时,将盒子的布局有效标志设为布局无效,并将盒子的固有尺寸有效标志设为固有尺寸无效。

如果计算样式的更改影响了LayoutConstraints对象内的值,则只需将盒子的固有尺寸有效标志设为固有尺寸无效。

注意:由于LayoutConstraints对象仅在布局函数中传递,因此无需使固有尺寸失效。

注意:以下属性的更改可能会影响LayoutEdges对象:

以下属性的更改可能会影响LayoutConstraints对象:

注意:这仅描述了与CSS布局API相关的布局失效。所有盒子从概念上都有一个布局有效标志,这些更改通过盒子树传播。

5.3. 布局 Worklet

layoutWorklet属性允许访问负责与布局相关的所有类的Worklet。

layoutWorklet的worklet 全局作用域类型为LayoutWorkletGlobalScope。

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

LayoutWorkletGlobalScope是layoutWorklet的全局执行上下文。

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

5.4. 注册布局

[Exposed=LayoutWorklet]
dictionary LayoutOptions {
  ChildDisplayType childDisplay = "block";
  LayoutSizingMode sizing = "block-like";
};

[Exposed=LayoutWorklet]
enum ChildDisplayType {
    "block",
    "normal",
};

[Exposed=LayoutWorklet]
enum LayoutSizingMode {
    "block-like",
    "manual",
};

文档有一个 映射,存储文档布局定义。初始时该映射为空;当调用 registerLayout(name, layoutCtor) 时填充。

LayoutWorkletGlobalScope 有一个 映射,存储布局定义。初始时该映射为空,当调用 registerLayout(name, layoutCtor) 时填充。

表示 布局 API 容器 的每个 盒子都有一个 映射,存储布局类实例。初始时该映射为空,当用户代理调用 确定内在尺寸 或 生成片段 时填充。

当调用 registerLayout(name, layoutCtor) 方法时,用户代理 必须 执行以下步骤:
  1. 如果 name 是一个空字符串,抛出 一个 TypeError 并终止所有这些步骤。

  2. 令 layoutDefinitionMap 为 LayoutWorkletGlobalScope 的 布局定义 映射。

  3. 如果 layoutDefinitionMap[name] 存在,抛出 一个 "InvalidModificationError" DOMException 并终止所有这些步骤。

  4. 令 inputProperties 为一个空的 sequence<DOMString>。

  5. 令 inputPropertiesIterable 为 Get(layoutCtor, "inputProperties") 的结果。

  6. 如果 inputPropertiesIterable 不为 undefined,则将 inputProperties 设置为 转换 inputPropertiesIterable 为 sequence<DOMString> 的结果。如果抛出异常,则重新抛出该异常并终止所有这些步骤。

    注意: 由输入属性获取器提供的 CSS 属性列表可以是自定义的或原生的 CSS 属性。

    注意: CSS 属性列表可能包含简写属性。

    注意: 为了使布局类向前兼容,CSS 属性列表还可以包含当前对用户代理无效的属性。例如 margin-bikeshed-property。

  7. 令 childInputProperties 为一个空的 sequence<DOMString>。

  8. 令 childInputPropertiesIterable 为 Get(layoutCtor, "childInputProperties") 的结果。

  9. 如果 childInputPropertiesIterable 不为 undefined,则将 childInputProperties 设置为 转换 childInputPropertiesIterable 为 sequence<DOMString> 的结果。如果抛出异常,则重新抛出该异常并终止所有这些步骤。

  10. 令 layoutOptionsValue 为 Get(layoutCtor, "layoutOptions") 的结果。

  11. 令 layoutOptions 为 转换 layoutOptionsValue 为 LayoutOptions 的结果。如果抛出异常,则重新抛出该异常并终止所有这些步骤。

  12. 如果 IsConstructor(layoutCtor) 的结果为 false,抛出 一个 TypeError 并终止所有这些步骤。

  13. 令 prototype 为 Get(layoutCtor, "prototype") 的结果。

  14. 如果 Type(prototype) 的结果不是 Object,抛出 一个 TypeError 并终止所有这些步骤。

  15. 令 intrinsicSizes 为 Get(prototype, "intrinsicSizes") 的结果。

  16. 如果 IsCallable(intrinsicSizes) 的结果为 false,抛出 一个 TypeError 并终止所有这些步骤。

  17. 如果 intrinsicSizes 的 [[FunctionKind]] 内部槽不是 "generator",抛出 一个 TypeError 并终止所有这些步骤。

  18. 令 layout 为 Get(prototype, "layout") 的结果。

  19. 如果 IsCallable(layout) 的结果为 false,抛出 一个 TypeError 并终止所有这些步骤。

  20. 如果 layout 的 [[FunctionKind]] 内部槽不是 "generator",抛出 一个 TypeError 并终止所有这些步骤。

  21. 令 definition 为一个新的 布局定义,其属性为:

  22. 设置 layoutDefinitionMap[name] 为 definition。

  23. 排队任务 以运行以下步骤:

    1. 令 documentLayoutDefinitionMap 为相关的 文档的 文档布局定义 映射。

    2. 令 documentDefinition 为一个新的 文档布局定义,其属性为:

    3. 如果 documentLayoutDefinitionMap[name] 存在,执行以下步骤:

      1. 令 existingDocumentDefinition 为 获取 documentLayoutDefinitionMap[name] 的结果。

      2. 如果 existingDocumentDefinition 是 "invalid",终止所有这些步骤。

      3. 如果 existingDocumentDefinition 和 documentDefinition 不相等(即 输入属性、子输入属性 和 布局选项 不同),则:

        设置 documentLayoutDefinitionMap[name] 为 "invalid"。

        向调试控制台记录错误,指出相同的类已使用不同的 inputProperties、childInputProperties 或 layoutOptions 注册。

    4. 否则,设置 documentLayoutDefinitionMap[name] 为 documentDefinition。

注意:类的结构应为:
class MyLayout {
    static get inputProperties() { return ['--foo']; }
    static get childrenInputProperties() { return ['--bar']; }
    static get layoutOptions() {
      return {childDisplay: 'normal', sizing: 'block-like'}
    }

    *intrinsicSizes(children, edges, styleMap) {
        // Intrinsic sizes code goes here.
    }

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

5.5. 布局引擎

问题:当前的 API 是可迭代生成器形式。基于实现经验和 web 开发者的经验,这可能会更改为基于 Promise 的 API。这两者各有优缺点。

Promises

生成器

5.5.1. 请求对象

[Exposed=LayoutWorklet]
        interface IntrinsicSizesRequest {
        };
        
        [Exposed=LayoutWorklet]
        interface LayoutFragmentRequest {
        };
        
        typedef (IntrinsicSizesRequest or LayoutFragmentRequest)
            LayoutFragmentRequestOrIntrinsicSizesRequest;
        

IntrinsicSizesRequest 具有内部槽:

LayoutFragmentRequest 具有内部槽:


作者提供的布局类上的 layout 方法和固有尺寸方法是生成器函数,而不是常规的 JavaScript 函数。这样做是为了让用户代理支持异步和并行布局引擎。

当作者在 layoutNextFragment() 方法上调用 LayoutChild 时, 用户代理不会同步生成一个 LayoutFragment 来返回给作者的代码。 相反,它会返回一个 LayoutFragmentRequest。 对作者而言,这个对象是完全不透明的,但它包含内部槽,封装了 layoutNextFragment() 方法调用。

当从布局生成器对象中生成一个或多个 LayoutFragmentRequest 时, 用户代理的布局引擎可能与其他工作异步运行该算法,和/或在不同的执行线程上运行。当引擎生成一个或多个 LayoutFragment 时, 用户代理将用生成的 LayoutFragment 来“触发”生成器对象。

同样适用于 intrinsicSizes() 方法。

下面显示了一个用 JavaScript 编写的布局引擎示例。
class LayoutEngine {
  // This function takes the root of the box-tree, a LayoutConstraints object, and a
  // BreakToken to (if paginating for printing for example) and generates a
  // LayoutFragment.
  layoutEntry(rootBox, rootPageConstraints, breakToken) {
    return layoutFragment({
      box: rootBox,
      layoutConstraints: rootPageConstraints,
      breakToken: breakToken,
    });
  }

  // 该函数接收一个 LayoutFragmentRequest 并调用相应的函数
  // layout algorithm to generate the a LayoutFragment.
  layoutFragment(fragmentRequest) {
    const box = fragmentRequest.layoutChild;
    const algorithm = selectLayoutAlgorithmForBox(box);
    const fragmentRequestGenerator = algorithm.layout(
        fragmentRequest.layoutConstraints,
        box.children,
        box.styleMap,
        fragmentRequest.breakToken);

    let nextFragmentRequest = fragmentRequestGenerator.next();

    while (!nextFragmentRequest.done) {
        // 用户代理可能会决定执行布局以生成片段
      // parallel on separate threads. This example performs them synchronously
      // in order.
      let fragments = nextFragmentRequest.value.map(layoutFragment);

      // 用户代理可能会在恢复此布局工作之前中断去执行其他工作(例如垃圾回收)
      // 此示例只是同步执行布局,无法进行中断。
      nextFragmentRequest = fragmentRequestGenerator.next(fragments);
    }

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

5.6. 执行布局

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

dictionary IntrinsicSizesResultOptions {
    double maxContentSize;
    double minContentSize;
};

5.6.1. 确定固有尺寸

确定固有尺寸算法定义了用户代理如何查询作者定义的布局,获取盒子的固有尺寸信息。

注意: 确定固有尺寸算法允许用户代理缓存任意数量的先前调用以便重用。

当用户代理希望为给定的box、childBoxes 确定固有尺寸的布局API格式化上下文时,它必须执行以下步骤:
  1. 令layoutFunction为box的布局()。

  2. 如果layoutFunction的固有尺寸有效标志为固有尺寸有效,则用户代理可以使用先前调用中的固有尺寸。如果是这样,它可以中止所有这些步骤并使用先前的固有尺寸值。

  3. 将layoutFunction的固有尺寸有效标志设置为固有尺寸有效。

  4. 令name为layoutFunction的第一个参数。

  5. 令documentDefinition为获取文档布局定义给定的name的结果。

    如果获取文档布局定义返回失败,或documentDefinition为"invalid",则让box回退到流布局,并中止所有这些步骤。

  6. 令workletGlobalScope为来自布局Worklet中工作域的WorkletGlobalScopes列表中的LayoutWorkletGlobalScope。

    用户代理必须拥有并从至少两个LayoutWorkletGlobalScope中选择,除非用户代理处于内存限制下。

    注意: 这是为了确保作者不依赖于能够在全局对象上存储状态或不可再生成的类状态。

    用户代理也可以在此时创建一个 WorkletGlobalScope,给定布局Worklet。

  7. 运行调用固有尺寸回调给定的name、box、childBoxes和workletGlobalScope,可选地并行。

    注意: 如果用户代理在调用固有尺寸回调时在一个线程上并行运行,它应选择可以在该线程上使用的布局工作域。

当用户代理希望为name、box、childBoxes和workletGlobalScope调用固有尺寸回调时,它必须执行以下步骤:
  1. 令definition为获取布局定义给定的name和workletGlobalScope的结果。

    如果获取布局定义返回失败,则让box回退到流布局,并中止所有这些步骤。

  2. 令layoutInstance为获取布局类实例给定box、definition和workletGlobalScope的结果。

    如果获取布局类实例返回失败,则让box回退到流布局,并中止所有这些步骤。

  3. 令inputProperties为definition的输入属性。

  4. 令children为一个新的列表。

  5. 对于每个childBox在childBoxes中执行以下子步骤:

    1. 令layoutChild为获取布局子元素给定workletGlobalScope、name和childBox的结果。

    2. 追加layoutChild到children。

  6. 令edges为一个新的LayoutEdgeSizes,其填充了box的所有计算值的盒模型边缘。

  7. 令styleMap为一个新的StylePropertyMapReadOnly,仅填充了box的inputProperties列表中的计算值。

    我们可能希望将styleMap存储在box上,类似于layoutInstance。

  8. 在此阶段,如果children、styleMap等同于先前的调用,用户代理可以重用先前调用中的固有尺寸。如果是这样,则让固有尺寸为缓存的固有尺寸,并中止所有这些步骤。

  9. 令intrinsicSizesGeneratorFunction为definition的固有尺寸生成器函数。

  10. 令intrinsicSizesGenerator为调用(intrinsicSizesGeneratorFunction,layoutInstance,«children,edges,styleMap»)的结果。

    如果抛出异常,则让box回退到流布局,并中止所有这些步骤。

  11. 令intrinsicSizesValue为运行生成器给定的intrinsicSizesGenerator和"intrinsic-sizes"的结果。

    如果运行生成器返回失败,则让box回退到流布局,并中止所有这些步骤。

  12. 令intrinsicSizes为转换intrinsicSizesValue为IntrinsicSizesResultOptions的结果。

    如果抛出异常,则让box回退到流布局,并中止所有这些步骤。

  13. 设置box的固有尺寸:

5.6.2. 生成片段

生成 片段算法定义了用户代理应如何为作者定义的布局生成盒的片段。

注:生成片段算法允许用户代理缓存任意数量的 先前调用以供复用。

当用户代理要为给定的 box、childBoxes、internalLayoutConstraints 和 可选的 internalBreakToken 生成布局 API 格式化 上下文的片段时,它必须运行以下步骤:
  1. 令 layoutFunction 为 box 的 <display-inside> 的计算值对应的 layout()。

  2. 如果 layoutFunction 的布局 有效标志为 layout-valid,则用户 代理可以使用上一次调用中的固有尺寸。如果这样做,它可以 中止所有这些步骤并使用之前的固有尺寸值。

  3. 将 layoutFunction 的布局 有效标志设置为 layout-valid。

  4. 令 name 为 layoutFunction 的第一个参数。

  5. 令 documentDefinition 为以 name 为参数获取 文档布局定义的结果。

    如果获取文档布局定义返回 失败,或者 documentDefinition 为 "invalid",则令 box 回退到流 布局并中止所有 这些步骤。

  6. 令 workletGlobalScope 为布局 Worklet 的 worklet 的 WorkletGlobalScopes 列表中的一个 LayoutWorkletGlobalScope。

    用户代理必须拥有至少两个 LayoutWorkletGlobalScope, 并从worklet 的 WorkletGlobalScopes 列表中进行选择, 除非用户代理受到内存限制。

    注:这是为了确保作者不会依赖于 能够在全局 对象上存储状态,或在类上存储无法重新生成的状态。

    用户代理此时也可以在给定布局 Worklet 的情况下创建 WorkletGlobalScope。

  7. 以 name、box、childBoxes、internalLayoutConstraints、 internalBreakToken 和 workletGlobalScope 为参数运行调用布局回调,并可选择并行运行。

    注:如果用户代理在某个线程上并行运行调用固有尺寸回调, 则它应选择一个可在该 线程上使用的布局 worklet 全局作用域。

当用户代理要在给定 name、box、 childBoxes、internalLayoutConstraints、internalBreakToken 和 workletGlobalScope 的情况下调用布局回调时,它必须运行 以下步骤:
  1. 令 definition 为以 name 和 workletGlobalScope 为参数获取布局 定义的结果。

    如果获取布局定义返回失败,则令 box 回退到流 布局并中止所有这些步骤。

  2. 令 layoutInstance 为以 box、definition、 workletGlobalScope 为参数获取布局 类实例的结果。

    如果获取布局类实例返回失败, 则令 box 回退到流 布局并中止所有这些步骤。

  3. 令 sizingMode 为 definition 的布局选项的 sizing 属性。

  4. 令 inputProperties 为 definition 的输入属性。

  5. 令 children 为一个新的列表。

  6. 对于 childBoxes 中的每个 childBox,执行 以下子步骤:

    1. 令 layoutChild 为以 workletGlobalScope、name 和 childBox 为参数获取布局子项 的结果。

    2. 将 layoutChild 追加到 children。

  7. 令 edges 为一个新的 LayoutEdgeSizes, 并使用 box 的所有盒 模型边缘的计算值填充它。

  8. 令 layoutConstraints 为以 internalLayoutConstraints、box 和 sizingMode 为参数创建布局约束对象的结果。

  9. 令 styleMap 为一个新的 StylePropertyMapReadOnly, 并且仅使用 box 的 inputProperties 中所列属性的计算值填充它。

    我们可能希望 改为将 styleMap 存储在 box 上,类似于 layoutInstance。

  10. 令 breakToken 为一个新的 BreakToken, 并使用来自 internalBreakToken 的适当信息填充它。

    如果 internalBreakToken 为 null,则令 breakToken 为 null。

  11. 在此阶段,如果 children、styleMap、layoutConstraints、 breakToken 与之前调用中的值等价,则用户代理可以复用先前调用中的片段。 如果这样做,则令片段输出为该缓存片段并中止所有这些步骤。

  12. 令 layoutGeneratorFunction 为 definition 的布局生成器函数。

  13. 令 layoutGenerator 为 调用(layoutGeneratorFunction, layoutInstance, «children, edges, layoutConstraints, styleMap, breakToken») 的结果。

    如果抛出异常,则令 box 回退到流布局并中止 所有这些步骤。

  14. 令 fragmentValue 为以 layoutGenerator 和 "layout" 为参数运行生成器的结果。

    如果运行 生成器返回失败,则令 box 回退到流 布局并中止所有这些步骤。

  15. 令 fragment 为将 fragmentValue 转换为 FragmentResultOptions 的结果。 如果抛出异常,则令 box 回退到流布局并中止所有这些步骤。

  16. 对于 fragment 的 childFragments 中的每个 childFragment, 执行以下子步骤:

    1. 如果 childFragment 的 [[generator]] 内部槽不等于 layoutGenerator, 则令 box 回退到流布局并中止所有这些步骤。

  17. 如果 sizingMode 为 "block-like":

    • 则:

      1. 令 inlineSize 为 layoutConstraints 的 fixedInlineSize。 (如果我们使用 "block-like" 尺寸计算,则 必须设置此值)。

      2. 令 blockSize 为按照与块容器完全相同的方式计算 box 的 边框盒块尺寸(相对于 box 的 书写模式)的结果,并将 fragment 的 autoBlockSize 作为“固有 高度”。

    • 否则(sizingMode 为 "manual"):

      1. 令 inlineSize 为 fragment 的 inlineSize。

      2. 令 blockSize 为 fragment 的 blockSize。

  18. 返回 box 的一个片段,其中:

5.6.3. 实用算法

本节指定了确定固有尺寸和生成片段算法中通用的算法。

当用户代理希望获取文档布局定义时,它必须执行以下步骤:
  1. 令documentLayoutDefinitionMap为关联的文档的文档布局定义映射。

  2. 如果 documentLayoutDefinitionMap[name] 不 存在,则返回失败并中止所有这些步骤。

  3. 返回获取的documentLayoutDefinitionMap[name]的结果。

当用户代理希望获取布局定义时,给定name和workletGlobalScope,它必须执行以下步骤:
  1. 令layoutDefinitionMap为workletGlobalScope的布局定义映射。

  2. 如果 layoutDefinitionMap[name] 不 存在,则运行以下步骤:

    1. 排队任务以执行以下步骤:

      1. 令documentLayoutDefinitionMap为关联的文档的文档布局定义映射。

      2. 设置documentLayoutDefinitionMap[name]为"invalid"。

      3. 用户代理应在调试控制台中记录一个错误,指出类未在所有LayoutWorkletGlobalScope中注册。

    2. 返回失败并中止所有这些步骤。

  3. 返回获取layoutDefinitionMap[name]的结果。

当用户代理希望获取布局类实例时,给定box、definition和workletGlobalScope,它必须执行以下步骤:
  1. 令layoutClassInstanceMap为box的布局类实例映射。

  2. 令layoutInstance为获取layoutClassInstanceMap[workletGlobalScope]的结果。如果layoutInstance为null,执行以下步骤:

    1. 如果definition的构造函数有效标志为false,则返回失败并中止所有这些步骤。

    2. 令layoutCtor为definition的类构造函数。

    3. 令layoutInstance为构造(layoutCtor)的结果。

      如果构造抛出异常,则将definition的构造函数有效标志设置为false,然后返回失败并中止所有这些步骤。

    4. 设置layoutClassInstanceMap[workletGlobalScope]为layoutInstance。

  3. 返回layoutInstance。

当用户代理希望运行生成器时,给定generator和generatorType,它必须执行以下步骤:
  1. 令done为布尔值,初始化为false。

  2. 令nextValue为未定义。

  3. 执行以下子步骤,直到done为true:

    1. 令nextFunction为获取的generator中的"next"的结果。

    2. 如果IsCallable(nextFunction)的结果为false,抛出TypeError并中止所有这些步骤。

    3. 令nextResult为调用Invoke(nextFunction,generator,«nextValue»)的结果。

      如果抛出异常,返回失败并中止所有这些步骤。

    4. 如果Type(nextResult)的结果不是Object,抛出TypeError并中止所有这些步骤。

    5. 令requestOrRequests为获取(nextResult中的"value"的结果)。

    6. 令done为获取的nextResult中的"done"的结果。

    7. 如果GetMethod(requestOrRequests,@@iterable)存在,则:

      1. 令results为一个新的空的列表。

      2. 令requests为转换的requestOrRequests为sequence<LayoutFragmentRequestOrIntrinsicSizesRequest>的结果。

        如果抛出异常,重新抛出该异常并中止所有这些步骤。

      3. 对于每个 request 在requests中,执行以下子步骤:

        1. 令result为生成生成器结果(request,generator,generatorType)的结果。

        2. 追加 result到results。

      4. 将nextValue设置为results。

      5. 继续。

    8. 令request为转换(requestOrRequests为LayoutFragmentRequestOrIntrinsicSizesRequest)的结果。

      如果抛出异常,重新抛出异常并中止所有这些步骤。

    9. 令result为生成生成器结果(request,generator,generatorType)的结果。

      如果生成生成器结果返回失败,返回失败并中止所有这些步骤。

    10. 将nextValue设置为result。

    11. 继续。

    用户代理可以乱序执行上面的循环,并且可以并行执行。但是requests和results的顺序必须保持一致。

    注意:这允许用户代理在不同线程或异步运行适当的布局算法(例如与其他工作时间切片布局工作)。如果用户代理并行执行循环,则外部循环必须等待所有跨线程任务完成后才能再次调用生成器。它不能向作者返回部分结果。

  4. 返回调用获取的nextResult中的"value"的结果。

当用户代理希望生成生成器结果时,给定request、generator和generatorType,它必须执行以下步骤:
  1. 如果request是IntrinsicSizesRequest,则:

    1. 令layoutChild为在request上查找内部槽[[layoutChild]]的结果。

    2. 令box为在layoutChild上查找内部槽[[box]]的结果。

    3. 如果box未附加到盒子树,返回失败并中止所有这些步骤。

      注意:作者可能会保留来自先前调用的LayoutChild。假设一个盒子只能附加到盒子树一次,且不会被重复使用。

    4. 令internalIntrinsicSizes为用户代理计算的box的边框盒最小/最大内容贡献的结果。

    5. 返回创建固有尺寸对象(request和internalIntrinsicSizes)的结果。

  2. 如果request是LayoutFragmentRequest,并且generatorType为"layout",则:

    1. 令layoutChild为在request上查找内部槽[[layoutChild]]的结果。

    2. 令box为在layoutChild上查找内部槽[[box]]的结果。

    3. 如果box未附加到盒子树,返回失败并中止所有这些步骤。

      注意:作者可能会保留来自先前调用的LayoutChild。假设一个盒子只能附加到盒子树一次,且不会被重复使用。

    4. 令childLayoutConstraints为在request上查找内部槽[[layoutConstraints]]的结果。

    5. 令childBreakToken为在request上查找内部槽[[breakToken]]的结果。

    6. 令internalFragment为用户代理基于box、childLayoutConstraints和childBreakToken生成的片段的结果。

    7. 返回创建布局片段(generator、request和internalFragment)的结果。

  3. 返回失败(以上分支均未被采用)。

6. 安全性考虑

这些功能没有已知的安全问题。

7. 隐私考虑

这些功能没有已知的隐私问题。

一致性

文档约定

一致性要求通过描述性断言和 RFC 2119 术语的组合来表达。规范中的“必须(MUST)”、“不得(MUST NOT)”、“必需(REQUIRED)”、“应(SHALL)”、“不应(SHALL NOT)”、“应当(SHOULD)”、“不应当(SHOULD NOT)”、“推荐(RECOMMENDED)”、“可以(MAY)”和“可选(OPTIONAL)”等关键字,应按照 RFC 2119 中的定义进行解释。不过,为了可读性,这些词语在本规范中不会全部使用大写字母。

本规范的所有文本都是规范性的,除非明确标记为非规范性、示例和注释部分。[RFC2119]

本规范中的示例以“例如”开头,或与规范性文本分开,并用class="example"标记,例如:

这是一个说明性示例。

说明性注释以“注”开头,并与规范性文本分开,使用class="note"标记,例如:

注:这是一个说明性注释。

建议性内容是规范性部分,样式特别,以引起特别关注,并使用<strong class="advisement">标记,例如: 用户代理必须提供一个可访问的替代方案。

一致性类别

本规范的一致性定义分为三类:

样式表
指CSS 样式表。
渲染器
指用户代理(UA),其解释样式表的语义并渲染使用它们的文档。
创作工具
指用户代理(UA),用于编写样式表。

样式表如果符合本模块定义的通用 CSS 语法和每个特性定义的单独语法,则其符合本规范。

渲染器如果不仅按照适当的规范解释样式表,还正确解析并渲染文档,同时支持本规范中定义的所有功能,则符合本规范。不过,由于设备限制,用户代理无法正确渲染文档的情况不会使其不符合规范。(例如,用户代理不要求在单色显示器上渲染颜色。)

创作工具如果编写的样式表在语法上符合本模块的通用 CSS 语法和每个特性的单独语法,并且符合本模块描述的所有其他样式表一致性要求,则符合本规范。

部分实现

为了让作者能够利用前向兼容的解析规则分配回退值,CSS 渲染器必须将任何其无可用支持的 at-rules、属性、属性值、关键字和其他语法结构视为无效,并适当地忽略。特别是,用户代理不得选择性地忽略不支持的组件值并保留支持的值在同一个多值属性声明中:如果任何值被视为无效(不支持的值必须视为无效),CSS 要求忽略整个声明。

不稳定和专有功能的实现

为了避免与未来稳定的 CSS 功能发生冲突,CSS 工作组建议遵循最佳实践,以实现不稳定的功能和 CSS 的专有扩展。

非实验性实现

一旦规范达到候选推荐阶段,非实验性实现就变得可能,且实现者应发布未加前缀的实现,只要他们能够证明其符合规范正确实现了任何 CR 级别的功能。

为了建立并维护 CSS 在不同实现间的互操作性,CSS 工作组请求非实验性的 CSS 渲染器在发布任何未加前缀的 CSS 功能实现之前,向 W3C 提交实现报告(以及如果需要的话,提交用于该实现报告的测试用例)。提交给 W3C 的测试用例将由 CSS 工作组进行审查和修正。

关于提交测试用例和实现报告的更多信息,请访问 CSS 工作组网站:https://www.w3.org/Style/CSS/Test/。如有问题,请发送至 public-css-testsuite@w3.org 邮件列表。

索引

本规范定义的术语

引用定义的术语

参考文献

规范性引用

[CSS-BREAK-3]
Rossen Atanassov; Elika Etemad. CSS 碎片化模块第 3 级. 2017年2月9日. CR. URL: https://www.w3.org/TR/css-break-3/
[CSS-CASCADE-4]
Elika Etemad; Tab Atkins Jr.. CSS 级联与继承第 4 级. 2016年1月14日. CR. URL: https://www.w3.org/TR/css-cascade-4/
[CSS-FLEXBOX-1]
Tab Atkins Jr.; Elika Etemad; Rossen Atanassov. CSS 弹性盒布局模块第 1 级. 2017年10月19日. CR. URL: https://www.w3.org/TR/css-flexbox-1/
[CSS-GRID-1]
Tab Atkins Jr.; Elika Etemad; Rossen Atanassov. CSS 网格布局模块第 1 级. 2017年12月14日. CR. URL: https://www.w3.org/TR/css-grid-1/
[CSS-INLINE-3]
Dave Cramer; Elika Etemad; Steve Zilles. CSS 内联布局模块第 3 级. 2016年5月24日. WD. URL: https://www.w3.org/TR/css-inline-3/
[CSS-OVERFLOW-3]
David Baron; Florian Rivoal. CSS 溢出模块第 3 级. 2016年5月31日. WD. URL: https://www.w3.org/TR/css-overflow-3/
[CSS-PAGE-FLOATS-3]
Johannes Wilm. CSS 页面浮动. 2015年9月15日. WD. URL: https://www.w3.org/TR/css-page-floats-3/
[CSS-POSITION-3]
Rossen Atanassov; Arron Eicholz. CSS 定位布局模块第 3 级. 2016年5月17日. WD. URL: https://www.w3.org/TR/css-position-3/
[CSS-PSEUDO-4]
Daniel Glazman; Elika Etemad; Alan Stearns. CSS 伪元素模块第 4 级. 2016年6月7日. WD. URL: https://www.w3.org/TR/css-pseudo-4/
[CSS-RUBY-1]
Elika Etemad; Koji Ishii. CSS Ruby 布局模块第 1 级. 2014年8月5日. WD. URL: https://www.w3.org/TR/css-ruby-1/
[CSS-SIZING-3]
Tab Atkins Jr.; Elika Etemad. CSS 内在与外在尺寸模块第 3 级. 2018年3月4日. WD. URL: https://www.w3.org/TR/css-sizing-3/
[CSS-SYNTAX-3]
Tab Atkins Jr.; Simon Sapin. CSS 语法模块第 3 级. 2014年2月20日. CR. URL: https://www.w3.org/TR/css-syntax-3/
[CSS-TYPED-OM-1]
Shane Stephens; Tab Atkins Jr.. CSS 类型化 OM 第 1 级. 2017年8月1日. WD. URL: https://www.w3.org/TR/css-typed-om-1/
[CSS-VALUES-3]
Tab Atkins Jr.; Elika Etemad. CSS 值与单位模块第 3 级. 2016年9月29日. CR. URL: https://www.w3.org/TR/css-values-3/
[CSS-WRITING-MODES-3]
Elika Etemad; Koji Ishii. CSS 书写模式第 3 级. 2017年12月7日. CR. URL: https://www.w3.org/TR/css-writing-modes-3/
[CSS21]
Bert Bos; et al. 层叠样式表第 2 级修订版(CSS 2.1)规范. 2011年6月7日. REC. URL: https://www.w3.org/TR/CSS2/
[CSS22]
Bert Bos. 层叠样式表第 2 级修订版(CSS 2.2)规范. 2016年4月12日. WD. URL: https://www.w3.org/TR/CSS22/
[CSS3-DISPLAY]
Elika Etemad. CSS 显示模块第 3 级. 2017年7月20日. WD. URL: https://www.w3.org/TR/css-display-3/
[CSSOM-1]
Simon Pieters; Glenn Adams. CSS 对象模型(CSSOM). 2016年3月17日. WD. URL: https://www.w3.org/TR/cssom-1/
[DOM]
Anne van Kesteren. DOM 标准. 现行标准. URL: https://dom.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML 标准. 现行标准. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. 基础标准. 现行标准. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. 在 RFC 中指示需求级别的关键字. 1997年3月. 当前最佳实践. URL: https://tools.ietf.org/html/rfc2119
[WebIDL]
Cameron McCormack; Boris Zbarsky; Tobie Langel. Web IDL. 2016年12月15日. ED. URL: https://heycam.github.io/webidl/
[WORKLETS-1]
Ian Kilpatrick. 工作线程第 1 级. 2016年6月7日. WD. URL: https://www.w3.org/TR/worklets-1/

参考性引用

[CSS-BACKGROUNDS-3]
Bert Bos; Elika Etemad; Brad Kemper. CSS 背景与边框模块第 3 级. 2017年10月17日. CR. URL: https://www.w3.org/TR/css-backgrounds-3/
[CSS-MULTICOL-1]
Håkon Wium Lie; Florian Rivoal; Rachel Andrew. CSS 多列布局模块第 1 级. 2017年10月5日. WD. URL: https://www.w3.org/TR/css-multicol-1/

IDL 索引

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

    IntrinsicSizesRequest intrinsicSizes();
    LayoutFragmentRequest layoutNextFragment(LayoutConstraints constraints, ChildBreakToken breakToken);
};

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

    attribute double inlineOffset;
    attribute double blockOffset;

    readonly attribute any data;

    readonly attribute ChildBreakToken? breakToken;
};

[Exposed=LayoutWorklet]
interface IntrinsicSizes {
  readonly attribute double minContentSize;
  readonly attribute double maxContentSize;
};

[Constructor(optional LayoutConstraintsOptions options),Exposed=LayoutWorklet]
interface LayoutConstraints {
    readonly attribute double availableInlineSize;
    readonly attribute double availableBlockSize;

    readonly attribute double? fixedInlineSize;
    readonly attribute double? fixedBlockSize;

    readonly attribute double percentageInlineSize;
    readonly attribute double percentageBlockSize;

    readonly attribute double? blockFragmentationOffset;
    readonly attribute BlockFragmentationType blockFragmentationType;

    readonly attribute any data;
};

dictionary LayoutConstraintsOptions {
    double availableInlineSize = 0;
    double availableBlockSize = 0;

    double fixedInlineSize;
    double fixedBlockSize;

    double percentageInlineSize;
    double percentageBlockSize;

    double blockFragmentationOffset;
    BlockFragmentationType blockFragmentationType = "none";

    any data;
};

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 any data;
};

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

enum BreakType { "none", "line", "column", "page", "region" };

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

  readonly attribute double blockStart;
  readonly attribute double blockEnd;

  // Convenience attributes for the sum in one direction.
  readonly attribute double inline;
  readonly attribute double block;
};

[Exposed=LayoutWorklet]
interface LayoutEdges {
  readonly attribute LayoutEdgeSizes border;
  readonly attribute LayoutEdgeSizes scrollbar;
  readonly attribute LayoutEdgeSizes padding;

  readonly attribute LayoutEdgeSizes all;
};

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

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

[Exposed=LayoutWorklet]
dictionary LayoutOptions {
  ChildDisplayType childDisplay = "block";
  LayoutSizingMode sizing = "block-like";
};

[Exposed=LayoutWorklet]
enum ChildDisplayType {
    "block",
    "normal",
};

[Exposed=LayoutWorklet]
enum LayoutSizingMode {
    "block-like",
    "manual",
};

[Exposed=LayoutWorklet]
interface IntrinsicSizesRequest {
};

[Exposed=LayoutWorklet]
interface LayoutFragmentRequest {
};

typedef (IntrinsicSizesRequest or LayoutFragmentRequest)
    LayoutFragmentRequestOrIntrinsicSizesRequest;

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

dictionary IntrinsicSizesResultOptions {
    double maxContentSize;
    double minContentSize;
};

问题索引

问题: 目前 API 是一个可迭代的生成器形式。基于实现经验和 Web 开发者经验,这可能会更改为基于 Promise 的 API。每种形式都有其优点和缺点。

Promises

Generator

↵
我们可能希望将 styleMap 存储在 box 上,类似于 layoutInstance。 ↵
我们可能希望将 styleMap 存储在 box 上,类似于 layoutInstance。 ↵