Skip to content

Latest commit

 

History

History
262 lines (208 loc) · 12.5 KB

5.zh.md

File metadata and controls

262 lines (208 loc) · 12.5 KB

2014年9月17日

让我们构建一个浏览器引擎!

第6部分: 块布局

欢迎回到我关于构建玩具HTML呈现引擎的系列文章:

本文将继续我们在第5部分中开始的布局模块. 这次,我们将添加布局块框的功能.

这些是垂直堆叠的框,例如 标题和段落.

为简单起见,此代码仅实现正常流动: 没有浮动,没有绝对定位,也没有固定的定位.

遍历布局树

这段代码的入口点是layout函数,取一个 LayoutBox 并计算其尺寸. 我们将这个函数分解为三种情况,现在只实现其中一种:

    impl LayoutBox {
        // 布置一个盒子及其后代。
        fn layout(&mut self, containing_block: Dimensions) {
            match self.box_type {
                BlockNode(_) => self.layout_block(containing_block),
                InlineNode(_) => {} // TODO
                AnonymousBlock => {} // TODO
            }
        }

        // ...
    }

块的布局取决于包含块的尺寸. 对于正常流程中的块框,这只是框的父级. 对于根元素,它是浏览器窗口的大小 (或*"视口"*) .

您可能还记得上一篇文章中 块的宽度 取决于其父级,而其高度取决于其子级. 这意味着我们的代码在计算宽度时需要自顶向下遍历树,在父元素的宽度已知后,布置子元素和自下而上计算高度,以便在其子项之后计算父级的高度.

    fn layout_block(&mut self, containing_block: Dimensions) {
        // 子宽度可以取决于父宽度,因此我们需要计算
        // 在布置孩子之前,这个盒子的宽度。
        self.calculate_block_width(containing_block);

        // 确定盒子在容器内的位置。
        self.calculate_block_position(containing_block);

        // 递归地布置这个盒子的孩子。
        self.layout_block_children();

        // 父高可以取决于孩子的身高,所以`calculate_height`
        // 在孩子们布置好之后必须打电话给*。
        self.calculate_block_height();
    }

此函数执行布局树的单次遍历,在向下的路上进行宽度计算,在回来的路上进行高度计算. 一个真正的布局引擎可能会执行多个树遍历,一些是自上而下的,另一些是自下而上的.

计算宽度

宽度计算是块布局功能的第一步,也是最复杂的. 我会一步一步地走过去. 首先,我们需要CSSwidth属性的值和所有左右边缘大小:

    fn calculate_block_width(&mut self, containing_block: Dimensions) {
        let style = self.get_style_node();

        // `width`的初始值为`auto`。
        let auto = Keyword("auto".to_string());
        let mut width = style.value("width").unwrap_or(auto.clone());

        // margin,border和padding的初始值为0。
        let zero = Length(0.0, Px);

        let mut margin_left = style.lookup("margin-left", "margin", &zero);
        let mut margin_right = style.lookup("margin-right", "margin", &zero);

        let border_left = style.lookup("border-left-width", "border-width", &zero);
        let border_right = style.lookup("border-right-width", "border-width", &zero);

        let padding_left = style.lookup("padding-left", "padding", &zero);
        let padding_right = style.lookup("padding-right", "padding", &zero);

        // ...
    }

这使用了一个名为lookup的辅助函数,它只按 顺序尝试一系列值. 如果未设置第一个属性,则尝试第二个属性. 如果未设置,则返回给定的默认值. 这提供了一个不完整 (但简单) 速记属性和初始值的实现.

注意: 这类似于JavaScript或Ruby中的以下代码:

margin_left = style["margin-left"] || style["margin"] || zero;

由于孩子无法改变其父母的宽度,因此需要确保自己的宽度适合父母的宽度. CSS规范将此表示为一组限制以及解决它们的算法. 以下代码实现了该算法.

首先,我们将 边距,填充,边框和内容宽度 相加. 该to_px辅助方法将 长度 转换为其数值. 如果属性设置为'auto',它返回0,因此它不会影响总和.

    let total = [&margin_left, &margin_right, &border_left, &border_right,
                 &padding_left, &padding_right, &width].iter().map(|v| v.to_px()).sum();

这是盒子所需的最小水平空间. 如果这不等于容器宽度,我们需要调整一些东西以使其相等.

如果 宽度或边距 设置为'auto',他们可以 扩展或收缩 以适应可用空间. 按照规范,我们首先检查盒子是否太大. 如果是这样,我们将任何可扩展边距设置为零.

    // 如果宽度不是自动且总数大于容器,则将自动边距视为0。
    if width != auto && total > containing_block.content.width {
        if margin_left == auto {
            margin_left = Length(0.0, Px);
        }
        if margin_right == auto {
            margin_right = Length(0.0, Px);
        }
    }

如果盒子对于它的容器来说太大了溢出-overflows容器. 如果它太小,它会潜流-underflow留下额外的空间. 我们将计算下溢 - 容器中剩余的额外空间量. (如果此数字为负数,则实际上是溢出. )

    let underflow = containing_block.content.width - total;

我们现在遵循规范算法通过调整 可扩展尺寸 来消除任何溢出或下溢. 如果没有'auto'尺寸,我们调整右边距. (是的,这意味着 margin 可能是的属于溢出的情况!)

    match (width == auto, margin_left == auto, margin_right == auto) {
        // 如果值过度约束,请计算margin_right。
        (false, false, false) => {
            margin_right = Length(margin_right.to_px() + underflow, Px);
        }

        // 如果只有一个大小是auto,则其使用的值来自相等。
        (false, false, true) => { margin_right = Length(underflow, Px); }
        (false, true, false) => { margin_left  = Length(underflow, Px); }

        // 如果width设置为auto,则任何其他自动值变为0。
        (true, _, _) => {
            if margin_left == auto { margin_left = Length(0.0, Px); }
            if margin_right == auto { margin_right = Length(0.0, Px); }

            if underflow >= 0.0 {
                // 展开宽度以填充下溢。
                width = Length(underflow, Px);
            } else {
                // 宽度不能为负数。改为调整右边距。
                width = Length(0.0, Px);
                margin_right = Length(margin_right.to_px() + underflow, Px);
            }
        }

        // 如果margin-left和margin-right都是auto,则它们的使用值相等。
        (false, true, true) => {
            margin_left = Length(underflow / 2.0, Px);
            margin_right = Length(underflow / 2.0, Px);
        }
    }

此时,约束得到满足,和'auto'值已转换为长度. 结果是水平框尺寸使用过的值,我们将存储在布局树中. 你可以看到最终的代码layout.rs.

定位-position

下一步更简单. 此函数查找,重新生成的 边距/填充/边框 样式,并将这些 样式与包含块尺寸一起使用,以确定此块在页面上的位置.

    fn calculate_block_position(&mut self, containing_block: Dimensions) {
        let style = self.get_style_node();
        let d = &mut self.dimensions;

        // margin,border和padding的初始值为0。
        let zero = Length(0.0, Px);

        // 如果margin-top或margin-bottom是`auto`,则使用的值为零。
        d.margin.top = style.lookup("margin-top", "margin", &zero).to_px();
        d.margin.bottom = style.lookup("margin-bottom", "margin", &zero).to_px();

        d.border.top = style.lookup("border-top-width", "border-width", &zero).to_px();
        d.border.bottom = style.lookup("border-bottom-width", "border-width", &zero).to_px();

        d.padding.top = style.lookup("padding-top", "padding", &zero).to_px();
        d.padding.bottom = style.lookup("padding-bottom", "padding", &zero).to_px();

        d.content.x = containing_block.content.x +
                      d.margin.left + d.border.left + d.padding.left;

        // 将框放在容器中所有前面的框下面。
        d.content.y = containing_block.content.height + containing_block.content.y +
                      d.margin.top + d.border.top + d.padding.top;
    }

仔细看看最后一个声明,它设置了y位置. 这就是块布局具有独特的垂直堆叠行为. 为此,我们需要确保父母的content.height在布置每个孩子后更新.

孩子

这是递归列出 框内容 的代码. 当它循环通过子框时,它会跟踪总内容高度. 定位代码 (上图) 使用它来查找下一个孩子的垂直位置.

    fn layout_block_children(&mut self) {
        let d = &mut self.dimensions;
        for child in &mut self.children {
            child.layout(*d);
            // 跟踪高度,以便每个孩子都布置在之前的内容之下。
            d.content.height = d.content.height + child.dimensions.margin_box().height;
        }
    }

每个孩子占用的总垂直空间是其高度margin盒子,我们计算如下:

    impl Dimensions {
        // 内容区域覆盖的区域加上填充。
        fn padding_box(self) -> Rect {
            self.content.expanded_by(self.padding)
        }
        // 内容区域覆盖的区域加上填充和边框。
        fn border_box(self) -> Rect {
            self.padding_box().expanded_by(self.border)
        }
        // 内容区域覆盖的区域加上填充,边框和边距。
        fn margin_box(self) -> Rect {
            self.border_box().expanded_by(self.margin)
        }
    }

    impl Rect {
        fn expanded_by(self, edge: EdgeSizes) -> Rect {
            Rect {
                x: self.x - edge.left,
                y: self.y - edge.top,
                width: self.width + edge.left + edge.right,
                height: self.height + edge.top + edge.bottom,
            }
        }
    }

为简单起见,这没有实现margin 折叠. 真正的布局引擎将允许 一个框的下边距与下一个框的上边距 重叠,而不是将每个边框框完全放在前一个边框下方.

'hight'属性

默认情况下,框的高度等于其内容的高度. 但如果'height'属性设置为显式长度,我们将使用它:

    fn calculate_block_height(&mut self) {
        // 如果高度设置为显式长度,请使用该精确长度。
        // 否则,只需保留`layout_block_children`设置的值。
        if let Some(Length(h, Px)) = self.get_style_node().value("height") {
            self.dimensions.content.height = h;
        }
    }

这就是块布局算法的结论. 你现在可以运行layout()在一个样式的HTML文档上,它会吐出一堆宽度,高度,边距等的矩形. 很酷,对吗?

练习

雄心勃勃的实现者的一些额外想法:

  1. 折叠垂直边距.

  2. 相对定位.

  3. 并行化布局过程,并测量对性能的影响.

如果尝试并行化项目,则可能需要将 宽度计算和高度计算 分成两个不同的过程. 通过为每个子项生成单独的任务,宽度的 自上而下遍历 很容易并行化. 高度计算有点棘手,因为您需要返回并调整 每个孩子的y位置在兄弟姐妹布置后,.

未完待续ⅆ

感谢所有跟随这一步的人!

随着我进一步探索不熟悉的布局和渲染领域,这些文章花费的时间越来越长. 在我尝试使用 字体和图形代码 时,在下一部分之前会有更长的间隙,但我会尽快恢复该系列.

更新:第7部分现在准备好了.