YSU
YSU 内核

盒模型与布局树

DOM 树加上计算样式,变成一棵带尺寸与位置的盒子树。这一步是布局阶段的输入准备工作。

DOM 树加上计算样式,变成一棵带尺寸与位置的盒子树。这一步是布局阶段的输入准备工作。

盒子类型

pub enum BoxKind {
    Block,
    Inline,
    InlineBlock,      // 对外行内级,对内块容器
    Flex,
    ListItem,         // 会额外画一个标记
    Text(Box<str>),   // 装的是已按空白规则处理过的内容
    AnonymousBlock,   // 包住块容器里的行内级内容
    Table,
    TableRowGroup,
    TableRow,
    TableCell,
    TableCaption,
}

Display 的取值一一对应,多出来的是 AnonymousBlock

Text 变体装的是 Box<str> 而不是 String——它一旦建好就不再改,没有扩容的需求,省下 8 字节的容量字段。

匿名块盒

块容器(BlockListItemFlex)里混着行内内容时,规范要求把连续的行内内容包进一个没有对应元素的块盒:

<div>
  一些文字
  <span>行内元素</span>
</div>

div 是块容器,里面的文字与 span 都是行内级,于是被包进一个匿名块盒。这个盒子在 DOM 里没有对应节点,在样式上继承 div 的。

有一条例外:Flex 容器不做匿名包装。弹性布局的子项直接就是子项,display: flex 会块化(blockify)它的子元素——把行内子项当成 inline-block 处理,而不是包一层匿名块。

空白处理

pub fn process_text(/* ... */) -> /* ... */;

文本在进盒子树之前先过一遍空白规则,判据是 white-space

连续空白换行符自动折行
normal折叠成一个空格当空格
nowrap折叠当空格不会
pre保留保留不会
pre-wrap保留保留
pre-line折叠保留

「折叠」包括去掉行首行尾的空白。这一步在盒子树阶段做掉,布局阶段拿到的就是干净的文本。

文本片段

行里的每一段文字是一个 TextFragment

pub struct TextFragment {
    pub text: String,
    pub color: Color,
    pub font: TextStyle,
    pub underline: bool,
    /// 该片段是否保留空白。
    pub preserve_whitespace: bool,
    /// 这段文字来自哪个节点。
    pub node: Option<NodeId>,
}

preserve_whitespace 控制一件事:不保留时,整行只有空白的行盒不占高度也不参与绘制。这就是 HTML 里元素之间的换行与缩进不会撑开页面的原因。

<div>
  <p>a</p>
  <p>b</p>
</div>

这段 HTML 里 </p><p> 之间有一个换行和一个缩进,它们构成一个纯空白行。不处理的话两个段落之间会多出将近一行的高度。

node 字段是命中测试的入口。行内元素在盒子树里没有自己的盒子——文字被折进所在块的行里——所以想从坐标反查「这一点压在哪个链接上」,只能沿着文字片段的来源节点往上找祖先。

几何

pub struct Point { /* f64 */ }
pub struct Size { /* f64 */ }
pub struct Rect { /* f64 */ }
pub struct Edges { /* f64,四边 */ }

全部是 f64,坐标一律相对页面左上角,单位是逻辑像素。布局层里不出现像素比这个概念——那是渲染层的事。

Rect::contains半开区间:右边界与下边界上的点不算在矩形内。这样相邻的两个元素不会对边界上的同一个点同时命中,命中测试因此不需要处理「取哪个」的问题。

布局上下文

pub struct LayoutContext {
    pub viewport_width: f64,
    pub viewport_height: f64,
    pub root_font_size: f64,
}

默认 1280×800,根字号 16。vw / vh / vmin / vmaxrem 从这三个值算。

长度换算

pub fn resolve_length(&self, length: Length, containing: f64, font_size: f64) -> f64;
单位换算
px / 无单位零原值
%value / 100 * containing
em× font_size
rem× root_font_size
vw / vhvalue / 100 × 视口宽/高
vmin / vmax视口宽高的较小/较大者
ch / ex× font_size × 0.5
绝对单位按解析阶段的换算表

chex 是近似的。 代码注释里写了原因:没有真实字体度量可用,0.5em 是个常见的近似值。要算准得问整形器要字体的实际参数。

containing 的含义取决于是哪个属性:宽高与内边距用包含块的宽度,纵向的外边距也用宽度(规范如此),行高用元素自己的字号。

相对单位在哪儿换算

分两处:

字号相关的(em% 的字号)在层叠阶段。元素的字号要先定下来,em 才有基准。

其余在布局阶段。宽高的百分比要知道包含块宽度,vw 要知道视口,这些信息只有在布局时才有。

一个容易搞错的地方

margin: 0 automargin: 0 在「解析成像素」之后都是零。区别靠 LengthUnit::Auto——布局只在明确写了 auto 的那一边吃剩余空间。

如果判据写成「左右外边距都是零就居中」,那么每一个「有明确宽度、外边距为零」的块都会被错误地居中——#d { width: 200px } 在 800 宽的视口里会跑到 x=300 而不是靠左。真实的网页上凡是定宽容器、卡片、右对齐的按钮,位置全错。

这条在 Length::is_auto() 里体现,is_zero() 不把 auto 当成零。

On this page