2014年9月8日
这是关于编写简单HTML呈现引擎的系列文章中的最新内容:
本文将开始布局模块,它采用 样式树并将其转换为二维空间中的一组矩形. 这是一个很大的模块,所以我将把它分成几篇文章. 此外,我在本文中分享的一些代码可能需要在 编写后面部分的代码时 进行更改.
布局模块的输入是来自的样式树第4部分,它的输出又是另一棵树了布局树. 这使我们在 迷你渲染管道中更上一层楼:
我将首先讨论基本的 HTML/CSS 布局模型. 如果您已经学会了开发网页,您可能已经熟悉了这一点 - 但它可能与实现的观点略有不同.
布局就是这样盒子. 框是网页的矩形部分. 它有宽度,高度和页面上的位置. 这个矩形叫做内容区域,因为它是绘制盒子内容的地方. 内容可以是文本,图像,视频或其他框.
盒子也可能有填充-padding,边界-borders,和页边空白-margins围绕其内容区域. CSS规范有一个图样展示所有这些层如何组合在一起.
下面,罗宾逊建立 盒子的内容区域和周围区域 的结构. [rust 注意: f32
是32位浮点类型. ]
// CSS盒子模型。 所有尺码均为px。
struct Dimensions {
// 内容区域相对于原 document 的位置:
content: Rect,
// 周边边缘:
padding: EdgeSizes,
border: EdgeSizes,
margin: EdgeSizes,
}
struct Rect {
x: f32,
y: f32,
width: f32,
height: f32,
}
struct EdgeSizes {
left: f32,
right: f32,
top: f32,
bottom: f32,
}
**注意:**本节包含的图表,如果您在没有 相关视觉样式 的情况下阅读它们将没有意义. 如果您在 Feed阅读器 中阅读此内容,请尝试打开原始页面在常规浏览器选项卡中. 我还为使用 屏幕阅读器或其他辅助技术的人员 提供了文字说明.
CSSdisplay
属性 确定元素生成的框的类型. CSS定义了几种盒子类型,每种类型都有自己的布局规则. 我只想谈谈其中两个: 块-block和内联-inline.
我将使用这段 伪HTML 来说明差异:
<container>
<a></a>
<b></b>
<c></c>
<d></d>
</container>
块盒从顶部到底部 垂直放置 在容器内.
a, b, c, d { display: block; }
**描述:**下图显示了垂直堆栈中的四个矩形.
内联框水平放置在容器内,从左到右. 如果它们到达容器的右边缘,它们将环绕,并继续在下面的新线上.
a, b, c, d { display: inline; }
描述下图显示了框a
,b
,和c
从左到右的水平线和 方框d
在下一行.
每个盒子必须包含只要块孩子,或只要内联儿童. 当 DOM元素包含 块和内联子元素的混合时,布局引擎将插入匿名框分开这两种类型. (这些框是"匿名的",因为它们与 DOM树中的节点 无关. )
在此示例中,内联框b
和c
被匿名块框包围,以粉色显示:
a { display: block; }
b, c { display: inline; }
d { display: block; }
**描述:**下图显示了垂直堆栈中的三个框. 第一个被贴上标签a
;第二个包含标记为 水平行的两个方框b
和c
;堆栈中的第三个框被标记d
.
请注意,内容默认会垂直增长. 也就是说,将儿童 添加到容器通常会使其更高,而不是更宽. 另一种说法是,默认情况下,块或行的宽度 取决于 其容器的宽度,而容器的高度取决于 其子容器 的高度.
如果覆盖类似width
和height
属性的默认值,这会变得更复杂,和如果尝试想支持像垂直写作这样的功能更复杂.
布局树是一组框. 一个盒子有尺寸,它可能包含子盒子.
struct LayoutBox<'a> {
dimensions: Dimensions,
box_type: BoxType<'a>,
children: Vec<LayoutBox<'a>>,
}
框可以是 块节点,内联节点或匿名块框. (当我实现文本布局时,这将需要更改,因为换行可能会导致 单个内联节点分成多个框. 但它现在会做. )
enum BoxType<'a> {
BlockNode(&'a StyledNode<'a>),
InlineNode(&'a StyledNode<'a>),
AnonymousBlock,
}
要构建布局树,我们需要查看每个DOM节点的display
属性. 我添加了一些代码到style
模块获得的节点的display
值. 如果没有指定值,则返回初始值. 'inline'
enum Display {
Inline,
Block,
None,
}
impl StyledNode {
// 如果属性返回指定的属性值存在, 或者 `None`.
fn value(&self, name: &str) -> Option<Value> {
self.specified_values.get(name).map(|v| v.clone())
}
// `display`属性的值(默认为inline).
fn display(&self) -> Display {
match self.value("display") {
Some(Keyword(s)) => match &*s {
"block" => Display::Block,
"none" => Display::None,
_ => Display::Inline
},
_ => Display::Inline
}
}
}
现在我们可以遍历样式树,为每个节点构建一个LayoutBox
,然后为节点的子节点插入框. 如果一个节点display
属性设置为'none'
,那么它不包含在布局树中.
// 构建LayoutBoxes树,但不执行任何操作布局计算.
fn build_layout_tree<'a>(style_node: &'a StyledNode<'a>) -> LayoutBox<'a> {
// 创建根框。
let mut root = LayoutBox::new(match style_node.display() {
Block => BlockNode(style_node),
Inline => InlineNode(style_node),
DisplayNone => panic!("Root node has display: none.")
});
// 创建后代框。
for child in &style_node.children {
match child.display() {
Block => root.children.push(build_layout_tree(child)),
Inline => root.get_inline_container().children.push(build_layout_tree(child)),
DisplayNone => {} // 跳过节点`display: none;`
}
}
return root;
}
impl LayoutBox {
// 构造函数
fn new(box_type: BoxType) -> LayoutBox {
LayoutBox {
box_type: box_type,
dimensions: Default::default(), // 原来set all fields to 0.0
children: Vec::new(),
}
}
// ...
}
如果块节点 包含 内联子节点,请创建一个匿名块框以包含它. 如果连续有多个内联子项,请将它们全部放在同一个匿名容器中.
// 新的内联孩子应该去哪里。
fn get_inline_container(&mut self) -> &mut LayoutBox {
match self.box_type {
InlineNode(_) | AnonymousBlock => self,
BlockNode(_) => {
// 如果我们刚刚生成一个匿名者block box, keep using it.
// 否则,创建一个新的。
match self.children.last() {
Some(&LayoutBox { box_type: AnonymousBlock,..}) => {}
_ => self.children.push(LayoutBox::new(AnonymousBlock))
}
self.children.last_mut().unwrap()
}
}
}
这有点从 标准CSS中 以多种方式简化的盒子一代算法 例如,它不处理 内联框 包含 块级子项 的情况. 此外,如果块级节点仅具有内联子节点,则会生成不必要的匿名框.
哇,这比我预期的要长.
我想我现在就到此为止,但不要担心: 第6部分即将推出,并将涵盖 块级布局. 一旦 块布局 完成,我们就可以跳到管道的下一个阶段: 绘画!
我想我可能会这样做,因为我们终于可以看到 渲染引擎的输出是漂亮的图片 而不仅仅是数字. 但是,除非我们通过实现 内联布局和文本布局来 完成布局模块,否则图片将只是一堆彩色矩形.
如果我在绘画之前,没有实现这些,我希望以后回到他们身边.