YSU
YSU 内核

块级与行内布局

布局引擎里最核心的两个格式化上下文。crates/ysu/src/layout/engine.rs 有 32 条单元测试。

布局引擎里最核心的两个格式化上下文。crates/ysu/src/layout/engine.rs 有 32 条单元测试。

入口

pub struct LayoutEngine { /* ... */ }
pub struct LayoutTree { /* ... */ }

impl LayoutEngine {
    pub fn new(context: LayoutContext) -> Self;
    pub fn set_context(&mut self, context: LayoutContext);
    pub fn context(&self) -> LayoutContext;
    pub fn layout(&mut self, document: &Document, styles: &StyleMap) -> Option<LayoutTree>;
}

impl LayoutTree {
    pub fn document_height(&self) -> f64;
    pub fn descendants(&self) -> Vec<&LayoutBox>;
    pub fn hit_test(&self, x: f64, y: f64) -> Option<&LayoutBox>;
    pub fn fragment_node_at(&self, x: f64, y: f64) -> Option<NodeId>;
}

layout 的输入是 DOM 加样式表,输出是盒子树。set_context 换视口或根字号,之后要重新调 layout

块级布局

块级盒在纵向依次堆叠,每个占满包含块的内容宽度。

盒模型。 内容盒的宽度按 width 定,没有就撑满包含块;高度按内容算,height 指定时用指定的。paddingbordermargin 依次向外。

外边距合并。 两种:相邻兄弟之间、父子之间。取两者中较大的那个作为间隔。

<div style="margin-bottom: 20px"></div>
<div style="margin-top: 30px"></div>

这两块之间的间隔是 30 像素,不是 50。

父子合并发生在父元素没有边框、没有内边距、也没有建立新的格式化上下文时:子元素的上外边距会「穿出去」变成父元素的上外边距。

auto 外边距居中。 左右外边距写成 auto 时,在宽度确定的前提下平分剩余空间。

#a { width: 200px; margin: 0 auto }   /* 居中 */
#b { width: 200px }                   /* 靠左 */

这两个的差别只在 auto 上,判据是 LengthUnit::Auto纵向的 auto 未处理——规范里它可以用来做垂直居中,这里没有实现。

尺寸约束。 min-width / max-width / min-height / max-heightwidth / height 之后应用,括住结果。

box-sizing 未实现。 只有 content-box 语义——width: 200px; padding: 20px 的外框是 240 而不是 200。

行内布局

行内格式化上下文(IFC)把行内级内容排成一行行。

行盒的构建。 从包含块的左边界开始,逐个放行内级项:文字片段、行内块、行内弹性容器。放不下就换行——换行点在文字片段之间,或者在行内块之间。

行内块收缩到适配。 inline-block 的宽度是「内容需要的宽度」,不超过可用宽度。inline-flex 同理。

行内块的推进有一个值得知道的历史坑。 同一行上有多个行内块时,每个的宽度必须真的加到行游标上。曾经有一版把片段宽度写死成零,游标不前进,后面每个行内块都用同一个 x——全叠在一起。这个错藏了很久,因为既有的测试只放了一个行内块:只有一个的时候,位置取自它前面的文字宽度,看着是对的。

text-align left / right / center 生效。justify 解析得出来,但结算时和 left 走同一个分支——也就是两端对齐是空操作

空白行。 整行只有空白且不保留空白时,这个行盒不占高度也不绘制。

命中测试

pub fn hit_test(&self, x: f64, y: f64) -> Option<&LayoutBox>;
pub fn fragment_node_at(&self, x: f64, y: f64) -> Option<NodeId>;

两个方向,用途不同。

hit_test 走盒子:找包含该点的最深的盒子。它认的是块级的矩形

fragment_node_at 走文字片段:按点找到压在该点上的文字片段,返回它的来源节点。

链接命中必须用后者。 行内元素在盒子树里没有自己的盒子,所以「找包含该点的最深的盒子,从它往上找 <a>」这个办法对任何行内链接都找不到锚点——链接点击整个不灵。反着查才行:文字片段带着来源节点,顺着它往上找祖先就是链接。

布局树的平移

impl LayoutBox {
    pub fn translate(&mut self, dx: f64, dy: f64);
    pub fn translate_to(&mut self, x: f64, y: f64);
}

把一棵子树整体移动。看起来是个辅助方法,但它曾经漏掉一样东西:行盒与文字片段的位置存在 lines 里,与 rectcontentclip 是分开的字段。只平移前者的话,结果是框过去了、字还留在原地

现象很具体:带内边距或背景的行内元素(<code> 这类)被搬到行上之后,文字叠在页面顶部,框里是空的。

平移要覆盖全部位置字段:rectcontentclip,以及全部子盒,以及行盒与它们里面的片段。

缩放到适配

inline-blockinline-flex 的宽度计算分两趟:先按「内容需要多宽」量一遍,再用这个宽度做布局。第二趟的结果如果超过了可用宽度,就收缩到可用宽度。

这个做法对内容不太长的元素够用。真正的 shrink-to-fit 要处理「内容宽度依赖可用宽度」的循环,那是表格布局级别的难题。

还没做的

特性状态
float连字段都没有,全仓零命中
clear同上
position: relative 的偏移字段有,布局不读
position: absolute 脱流字段有,布局不读,按常规流处理
position: fixed同上
vertical-align字段都没有,行盒基线写死为行高的 0.8 倍
text-indent行内布局里没有首行缩进入口
box-sizing未实现
text-align: justify解析得出来,结算与 left 相同

position 那一组值得强调:字段解析进了计算样式,但布局与绘制阶段零读取position: absolute 的元素会像普通块一样参与常规流,占位置、参与外边距合并。

测试

32 条,覆盖盒模型各部分的计算、外边距合并的两种情形、auto 外边距的居中与不居中、行盒的构建与折行、行内块收缩到适配、空白行的处理、min/max 约束、以及 hit_testfragment_node_at 的边界。

On this page