布局概述

上次更新:2024 年 5 月

简介

大部分布局代码处理帧树(或渲染树)上的操作。在帧树中,每个节点表示一个矩形(或对于 SVG,其他形状)。帧树的形状类似于内容树,因为许多内容节点都有一个对应的帧,尽管它们在一些方面有所不同:一些内容节点有多个帧或根本没有帧。当元素在 CSS 中为 display:none 或由于某些其他原因未显示时,它们将没有任何帧。当元素跨行或页面断开时,它们有多个帧;当需要多个嵌套的帧来显示单个元素时,元素也可能有多个帧(例如,<table><video controls>)。

帧树中的每个节点都是从 nsIFrame 派生的类的实例。与内容树一样,存在一个庞大的类型层次结构,但类型层次结构非常不同:它包括文本帧、块和内联、表格的各个部分、弹性盒和网格容器以及各种类型的 HTML 表单控件等类型。

帧分配在 PresShell 拥有的一个区域内。每个帧由其父级拥有,在 nsCSSFrameConstructor 中创建,并通过 nsIFrame::Destroy() 销毁。帧不是引用计数的,代码不得保留指向帧的指针。为了减轻访问已销毁帧的指针时可能出现的安全漏洞,我们使用 帧中毒,它分为两个部分。当帧在呈现结束之外被销毁时,我们会用一个重复指向不可访问内存的指针模式填充其内存,然后将内存放到每个帧类的空闲列表中。这意味着如果代码通过悬空指针访问内存,它要么会通过取消引用中毒模式快速崩溃,要么会找到一个有效的帧。

与内容树一样,只能从其进程的主线程访问帧。

帧树通常不应存储任何无法即时重新计算的数据。虽然帧树通常在页面显示时持续存在,但帧通常会因某些样式更改(例如将元素上的 display:block 更改为 display:flex)而被销毁和重新创建。

帧表示的矩形是 CSS 所称的元素的边框框。请参阅 CSS2 规范中 8.1 边框尺寸 中的插图。这是边框的外边缘(或边距的内边缘)。边距位于边框之外;填充位于边框之内。除了 nsIFrame::GetRect() 之外,我们还有 API nsIFrame::GetPaddingRect() 用于获取填充框(填充的外边缘或边框的内边缘)和 nsIFrame::GetContentRect() 用于获取内容框(内容的外边缘或填充的内边缘)。当需要重排(或尚未发生)时,这些 API 可能产生过时的结果。

墨水溢出与可滚动溢出

除了跟踪矩形之外,帧还跟踪两个溢出区域:**墨水溢出**和**可滚动溢出**。这些溢出区域表示帧及其所有后代所需的区域的并集。墨水溢出用于与绘制相关的优化:它是一个覆盖帧及其所有后代绘制时可能绘制的所有区域的矩形。可滚动溢出表示用户应该能够滚动到的区域,以查看帧及其所有后代。在某些情况下,帧的矩形与其溢出之间的差异是由于超出帧范围的后代引起的;在其他情况下,它们是由于帧本身的某些特性引起的。这两个溢出区域相似,但存在差异:例如,边距是可滚动溢出的一部分,但不是墨水溢出的一部分,而文本阴影是墨水溢出的一部分,但不是可滚动溢出的一部分。

碎片化简介 (或者为什么我们需要帧延续?)

当帧跨行、列或页面断开时,我们会创建多个帧来表示元素的多个矩形。第一个称为**主帧**,其余的称为其**延续帧**或简称**延续**(在重排期间更有可能被销毁和重新创建)。这些帧链接在一起作为延续:它们具有一个双向链接列表,可用于使用 nsIFrame::GetPrevContinuation()nsIFrame::GetNextContinuation() 遍历延续。(目前延续始终具有相同的样式数据,尽管我们可能在某个时候希望打破该不变性。)

延续有时是彼此的兄弟(即 nsIFrame::GetNextContinuation()nsIFrame::GetNextSibling() 可能会返回相同的帧),有时不是。例如,如果一个段落包含一个跨行拆分的链接的跨度,则跨度的延续是兄弟(因为它们都是段落的子节点),但链接的延续不是兄弟(因为链接的每个延续都来自跨度的不同延续)。遍历整个帧树**不需要**显式遍历任何帧的延续列表,因为所有延续都是包含断开的元素的后代。

我们还将延续用于延续在重排期间不应重新换行的情况(最重要的是双向重新排序,其中左右文本和右左文本需要分离到不同的延续中,因为它们可能不形成连续的矩形):我们将这些延续称为**固定**而不是**流体**。nsIFrame::GetNextInFlow()nsIFrame::GetPrevInFlow() 仅遍历流体延续,并且不跨越固定延续边界。我们将在后面的部分 碎片化 中详细解释延续和碎片化。

IB 分割

如果内联帧具有非内联子节点,则我们将原始内联帧拆分为多个部分。原始内联的子节点按如下方式分布到这些部分中:原始内联的子节点被分组为内联和非内联的运行,内联的运行获得内联父节点,而非内联的运行获得匿名块父节点。我们称之为“IB 分割”或“块内内联分割”。此分割递归地向上遍历帧树,直到所有内联中的非内联都是匿名块包装器之间具有块帧的祖先。此分割保持这些子帧之间的相对顺序,并且分割内联的各个部分之间的关系使用 IB 兄弟链维护。需要注意的是,在帧构建期间创建的任何包装器(例如表格)可能不会包含在 IB 兄弟链中,具体取决于此包装器创建发生的时间。请参阅 nsCSSFrameConstructor::CreateIBSiblings() 中的详细信息。

物理坐标与逻辑坐标

在西方文字中,文本从左到右流动,行和块容器从上到下前进。为了表示此书写模式下的矩形,我们很自然地选择空间中左上角的原点,即 (left,right)(0,0) 处。帧(矩形)的大小由其 (width, height) 指定。这称为“物理坐标”。

但是,为了支持网络上的各种国际书写模式,我们需要对该概念进行概括。例如,在中文或日文的垂直排版中,文本可以从上到下流动,而行可以从右到左前进;在蒙古文脚本中,文本可以从上到下流动,而行可以从左到右前进。我们定义“抽象坐标”或“逻辑坐标”来统一不同书写模式下的坐标。文本流动方向定义为“内联方向”,行或块容器堆叠的方向定义为“块方向”。CSS 定义了三个属性来确定书写模式:writing-modedirectiontext-orientation

几乎所有物理 CSS 属性都有其逻辑对应项。例如,widthheight 对应于 inline-sizeblock-sizeleftrighttopbottom 对应于 inline-startinline-endblock-startblock-end

在布局中,我们有物理类型,例如 nsPointnsSizensRectnsMargin;它们的逻辑对应项是 LogicalPointLogicalSizeLogicalRectLogicalMargin。理想情况下,我们应该都在逻辑坐标上工作,并将仍然使用物理坐标的代码转换为逻辑坐标,除非物理坐标可能更有意义。

参考资料

代码(请注意,base 和 generic 中的大多数文件顶部都有有用的单行描述,在 Searchfox 中浏览目录时会显示这些描述)

  • layout/base/ 包含协调所有内容的对象以及许多其他杂项内容

  • layout/generic/ 包含基本框架类以及对其重排方法的支持代码(ReflowInputReflowOutputnsReflowStatus

  • layout/forms/ 包含 HTML 表单控件的框架类

  • layout/tables/ 包含 CSS/HTML 表格的框架类

  • layout/mathml/ 包含 MathML 的框架类

  • layout/svg/ 包含 SVG 的框架类

  • layout/xul/ 包含 XUL 盒模型和各种 XUL 窗口部件的框架类

Bugzilla:所有名称以“Layout”开头且属于“Core”产品的组件。

更多文档

框架构建

框架构建是创建框架的过程,由 nsCSSFrameConstructor 处理。当样式以需要创建或重新创建框架的方式发生变化,或者当节点插入文档时,就会进行框架构建。内容树和框架树的形状并不完全相同,框架构建过程会完成一些创建框架树正确形状的工作。它处理创建正确形状的方面,这些方面不依赖于布局信息。例如,框架构建处理实现 表格匿名对象 所需的工作,但不处理元素跨行或跨页断开时需要创建的框架。

框架构建的基本单位是单个父元素的连续子元素的运行。当被要求为这样的子元素运行构建框架时,框架构造函数首先根据所涉及节点的兄弟节点和父节点确定新框架应该插入到框架树中的哪个位置。然后,框架构造函数遍历所涉及的内容节点列表,并为每个节点创建一个称为**框架构建项**的临时数据结构,即 FrameConstructionItem。框架构建项封装了创建内容节点框架所需各种信息:其样式数据、关于如何根据其命名空间、标签名称和样式创建此节点的框架的一些元数据,以及关于将创建哪种框架的一些数据。然后分析此框架构建项列表,以查看基于它构建框架并将其插入到选定的插入点是否会生成有效的框架树。如果不会,则框架构造函数要么修复框架构建项列表以便生成的框架树有效,要么丢弃框架构建项列表并请求销毁和重新创建父元素的框架,以便它有机会创建它*可以*修复的框架构建项列表。父元素的重新创建称为“重新构建”,这是一个代价高昂的操作,我们希望尽可能避免它。

一旦框架构造函数具有框架构建项列表和会导致有效框架树的插入点,它就会继续根据这些项创建框架。非叶框架的创建会递归尝试为该框架元素的子元素创建框架,因此实际上框架是在内容树的深度优先遍历中创建的。

因此,框架构造函数中的绝大多数代码都属于以下类别之一

  • 确定新框架在框架树中的正确插入点的代码。

  • 为给定的内容节点创建框架构建项的代码。这涉及到通过静态数据表搜索要创建的框架的元数据。

  • 分析框架构建项列表的代码。

  • 修复框架构建项列表的代码。

  • 从框架构建项创建框架的代码。

重排

重排是计算框架的位置和大小的过程。(毕竟,框架表示矩形,在某些时候我们需要弄清楚确切的**什么**矩形。)重排是递归完成的,每个框架的 Reflow() 方法调用该框架的后代的 Reflow() 方法。

在许多情况下,正确的结果由 CSS 规范(特别是 CSS 2.2)定义。在某些情况下,细节没有由 CSS 定义,尽管在某些(但不是全部)情况下,我们受到 Web 兼容性的约束。但是,当细节由 CSS 定义时,计算布局的代码的结构通常与 CSS 规范中描述的方式有所不同,因为 CSS 规范通常是用约束来编写的,而我们的布局代码由针对增量重新计算优化的算法组成。

重排从哪里开始?我们如何避免每次都重排整个世界?

重排通常从框架树的根开始,尽管其他一些类型的框架可以充当“重排根”并从它们开始重排(nsTextControlFrame 就是一个例子;请参阅 NS_FRAME_REFLOW_ROOT 框架状态位)。重排根必须遵守这样一个不变式:其后代内部的变化永远不会改变其矩形或溢出区域(尽管目前滚动条是重排根,但并不完全遵守此不变式)。

在许多情况下,我们希望重排框架树的一部分,并且希望此重排高效。例如,当内容添加到或从文档树中删除或样式发生变化时,我们希望需要重做的工作量与内容量成正比。我们还希望有效地处理对相同内容的一系列更改。为此,我们在框架上维护两个位:NS_FRAME_IS_DIRTY 指示框架及其所有后代都需要重排。NS_FRAME_HAS_DIRTY_CHILDREN 指示框架有一个后代是脏的或有一个后代被删除(有关详细信息,请参阅其注释)。这些位允许合并多个更新;此合并在 PresShell 中完成,PresShell 跟踪需要重排的重排根集。这些位在调用 PresShell::FrameNeedsReflow 期间设置,并在重排期间清除。

重排契约

许多框架类使用的布局算法是 CSS 中指定的算法,这些算法基于传统的文档格式化模型,其中内联大小(宽度)是输入,块大小(高度)是输出。

当调用单个框架的 Reflow() 方法时,大多数输入都在 ReflowInput 中提供,由父框架设置。输出填充到 ReflowOutputnsReflowStatus 中。重排后,调用方(通常是父级)负责根据 ReflowOutput 中报告的指标设置框架的大小。调用方还负责根据 nsReflowStatus 中报告的完成状态创建延续。我们将在后面 重排状态 部分中更详细地介绍 nsReflowStatus

计算内在大小

在某些情况下,需要根据内容确定内联大小。例如,具有 width:min-contentwidth:max-content 的元素。这取决于两个**内在内联大小**:最小内在内联大小(请参阅 nsIFrame::GetMinISize())和首选内在内联大小(请参阅 nsIFrame::GetPrefISize())。这些内联大小代表什么的概念最好通过描述它们在仅包含文本的段落中的含义来解释:在这样的段落中,最小内在内联大小是最长单词的内联大小,首选内在内联大小是在一行上布局的整个段落的内联大小。

内在内联大小与上面描述的脏位分开失效。当调用方通过 PresShell::FrameNeedsReflow() 通知预渲染外壳框架需要重排时,它会传递三个选项之一

  • None 指示没有内在内联大小是脏的

  • FrameAndAncestors 指示其自身及其祖先上的内在内联大小是脏的(例如,如果向其添加新的子元素,就会发生这种情况)

  • FrameAncestorsAndDescendants 指示其自身、其祖先及其后代上的内在内联大小是脏的(例如,如果字体大小发生变化)

绘制

请参阅 渲染概述

碎片化

碎片化(或分页)是打印、打印预览和多列布局中使用的概念。

框架树中的延续

为了渲染表示为 nsIContent 对象的 DOM 节点,Gecko 会创建零个或多个框架(nsIFrame 对象)。每个框架都表示一个矩形区域,通常对应于 CSS 规范中描述的节点的 CSS 框。简单的元素通常可以用恰好一个框架来表示,但有时一个元素需要用多个框架来表示。例如,文本跨行断开

  xxxxxx AAAA
  AAA xxxxxxx

A 元素是一个单个 DOM 节点,但显然单个矩形框架无法精确地表示其布局。

类似地,考虑文本跨页断开

  | BBBBBBBBBB |
  | BBBBBBBBBB |
  +------------+

  +------------+
  | BBBBBBBBBB |
  | BBBBBBBBBB |
  |            |

同样,单个矩形框架无法表示节点的布局。具有多个列的多列容器与此类似。

单个 DOM 节点由多个框架表示的另一种情况是,文本节点包含双向文本(例如,希伯来语和英语文本)。在这种情况下,文本节点及其内联祖先会被拆分,以便每个框架仅包含单向文本。

元素的第一个框架称为**主框架**。其他框架称为**延续框架**。主框架由 nsCSSFrameConstructor 响应内容插入通知创建。延续框架在双向解析期间创建,并且在重排期间创建,当重排检测到内容元素无法在分配的约束内完全布局时(例如,当内联文本不适合特定宽度约束,或者当块无法在特定高度约束内布局时)。

在重排期间创建的延续框架称为“流体”延续(或“内联”)。其他延续框架(目前,在双向解析期间创建的框架)则相反,是“非流体”。NS_FRAME_IS_FLUID_CONTINUATION 状态位指示延续框架是流体还是非流体。

元素的框架被放置在一个双向链表中。这些链接可以通过 nsIFrame::GetNextContinuation()nsIFrame::GetPrevContinuation() 访问。如果只需要访问流式延续,则使用 nsIFrame::GetNextInFlow()nsIFrame::GetPrevInFlow()

下图显示了仅考虑主框架的原始框架树与可能包含断裂和延续的布局之间的关系。

Original frame tree       Frame tree with A broken into three parts
    Root                      Root
     |                      /  |  \
     A                     A1  A2  A3
    / \                   / |  |    |
   B   C                 B  C1 C2   C3
   |  /|\                |  |  | \   |
   D E F G               D  E  F G1  G2

某些类型的框架会为同一个内容元素创建多个子框架。

  • nsPageSequenceFrame 创建多个页面子元素,每个子元素都与整个文档相关联,并由分页符分隔。

  • nsColumnSetFrame 创建多个块子元素,每个子元素都与列元素相关联,并由分栏符分隔。

  • nsBlockFrame 创建多个内联子元素,每个子元素都与同一个内联元素相关联,并由换行符或文本方向的变化分隔。

  • nsTableColFrame 如果其 span=”N” 且 N > 1,则会为自己创建非流式延续。

  • 如果一个块框架是多列容器,并且具有 column-span:all 子元素,则它会创建多个 nsColumnSetFrame 子元素,这些子元素作为非流式延续链接在一起。类似地,如果一个块框架位于多列格式化上下文中,并且具有 column-span:all 子元素,则它会被分割成多个流,这些流也作为非流式延续链接在一起。请参阅 nsCSSFrameConstructor::ConstructBlock() 中的文档和示例框架树。

溢出容器延续

有时,框架的内容需要跨页断开,即使框架本身是完整的。如果具有固定块大小的元素的溢出内容无法容纳在一页上,通常会发生这种情况。在这种情况下,已完成的框架为“溢出不完整”,并且会创建特殊的延续来保存其溢出内容。这些延续称为“溢出容器”。它们是不可见的,并保存在其父元素的特殊列表中。请参阅 nsContainerFrame.h 中的文档和 错误 379349 评论 3 中的示例树。

此基础结构在 错误 154892 中进行了扩展,以管理绝对定位框架的延续。

延续与框架树结构的关系

值得强调关于 prev-continuation / next-continuation 链接与现有框架树结构关系的两个要点。

首先,如果您想遍历框架树或其子树以检查所有框架一次,则应遍历 next-continuation 链接。所有延续都可以通过遍历所有子列表的 GetFirstChild() 结果的 GetNextSibling() 链接来访问。

其次,以下属性成立:考虑两个框架 F1 和 F2,其中 F1 的 next-continuation 是 F2,它们各自的父框架是 P1 和 P2。那么 P1 的 next continuation 是 P2,或者 P1 == P2,因为 P 负责断开 F1 和 F2。

换句话说,延续有时是彼此的兄弟姐妹,有时不是。如果它们的父内容在同一点断开,则它们不是兄弟姐妹,因为它们是父内容的不同延续的子元素。因此,对于标记的框架树

<p>This is <b><i>some<br/>text</i></b>.</p>

<b> 元素的两个延续是兄弟姐妹(除非换行符也是分页符),但 <i> 元素的两个延续不是。

当 F1 是第一个流式浮动占位符时,该属性有一个例外。在这种情况下,F2 的父元素将是 F1 的包含块的 next-in-flow。

重排状态

重排状态位于 Reflow()aStatus 参数中。IsComplete() 表示我们已重排所有内容,并且不再需要 next-in-flow。此时可能仍然存在 next-in-flow,但父元素将删除它们。IsIncomplete() 表示“某些内容不适合此框架”。IsOverflowIncomplete() 表示框架本身是完整的,但某些内容不适合:这会触发为框架的延续创建溢出容器。IsIncomplete()NextInFlowNeedsReflow() 表示“某些内容不适合此框架,并且必须对其进行重排”。这些值在 nsReflowStatus::Completion 中定义和记录。

动态重排注意事项

当我们使用流式延续重排框架 F 时,可能会发生两种情况。

  1. 某些子框架不适合传递的内联大小或块大小约束。这些框架必须“推送到”F 的 next-in-flow。如果 F 没有 next-in-flow,则必须在 F 的父元素的 next-in-flow 下创建它——或者如果 F 的父元素正在管理 F 的断开,则直接在 F 的父元素下创建 F 的 next-in-flow。如果 F 是一个块,它会将溢出的子框架推送到其“溢出”子列表,并强制对 F 的 next-in-flow 进行重排。当我们重排一个块时,我们会将子框架从 prev-in-flow 的溢出列表拉到当前框架中。

  2. 所有子框架都适合传递的内联大小或块大小约束。然后必须从 F 的 next-in-flow “拉取”子框架以填补可用空间。如果 F 的 next-in-flow 变得为空,我们可能能够删除它。

在这两种情况下,我们最终都可能得到一个框架 F,其中包含两个子框架,其中一个子框架是另一个子框架的延续。这是不正确的。我们也可能会创建空洞,其中存在框架 P1 P2 和 P3,P1 具有子框架 F1,而 P3 具有子框架 F2,但 P2 没有 F 子框架。

避免这些问题的策略如下:当从父元素 P2 将框架 F2 拉到 prev-in-flow P1 时,如果 F2 是一个可断开的容器,则

  • 如果 F2 在 P1 中没有 prev-in-flow F1,则在 P1 中为 F2 的内容创建一个新的主框架 F1,并以 F2 作为其 next-in-flow。

  • 将子元素从 F2 拉到 F1,直到 F2 为空或空间不足。如果 F2 变为空,则从下一个非空的 next-in-flow 中拉取。没有 next-in-flow 的空延续可以删除。

当将框架 F1 从父元素 P1 推送到 P2 时,其中 F1 具有 next-in-flow F2(它必须是 P2 的子元素)

  • 通过将所有 F2 的子元素移动到 F1 中,然后删除 F2 来将 F2 合并到 F1 中。

对于内联框架 F,我们有自己的自定义策略,可以合并相邻的内联框架。这不需要更改。

当 F 是一个普通的流式块、浮动块,以及最终的绝对定位块时,我们需要实现此策略。