Hello, I am the smiling snail, 🐌.

In the last article we looked at the generation of a style tree, which determines the style of each node. This section focuses on how to generate a layout tree, which is to determine the location of each node.

The box model

Let’s start with the box model, because it’s the base on which the layout unfolds.

To put it simply, the location area of a node is composed of four parts: content area, inner margin, border and margin, as shown in the figure below.

Box type

The display type set to the element determines the box type. The common ones are as follows:

  • Block, a block-level element that generates block-level boxes
  • Inline, an inline element, generates an inline box
  • None: indicates that the element is not displayed

block box

Block box, it’s a container. The default is vertical, with internal elements on a single line. As follows:

inline box

Inline box, which is also a container. The default is horizontal and displayed on a single line. When a line is full, it is folded to display. As shown below:

anonymous box

Anonymous boxes are special because they are not associated with actual elements, so they are called anonymous. An introduction to anonymous boxes can be found at link 1 at the end of this article.

Because a box container can contain only one type of box, either block or inline. Anonymous boxes are created when a box is mixed with elements of different layout types.

Anonymous box is divided into anonymous block box and anonymous in-line box.

1. Anonymous Block box

There are two scenarios for the generation of anonymous block boxes.

When a block box contains both a block and an inline element, an anonymous block box is generated around which the inline element is wrapped.

Here’s an example:

div, p {display: block}

<div>Some inline text <p>followed by a paragraph</p> followed by more inline text.</div>
Copy the code

Div and P are block-level elements, and the text before and after them are inline elements. At this point, two anonymous block boxes are generated to wrap the text before and after. As shown below:

Similarly, an anonymous block box is generated when a block element is included in an inline box.

Such as:

p    { display: inline }
span { display: block }

<p>
This is anonymous text before the SPAN.
<span>This is the content of SPAN.</span>
This is anonymous text after the SPAN.
</p>
Copy the code
  • pGenerate inline boxes,spanGenerate block-level boxes. At this point, inline boxes contain block-level boxes.
  • "This is anonymous text before the SPAN."Creates an anonymous block box.
  • "This is anonymous text after the SPAN."Anonymous block boxes are also generated.

As shown below:

2. Anonymous inline boxes

A common case is that when a block box contains text, it automatically generates anonymous inline box wrapped text.

Such as:

p    { display: block }

<p>Some <em>emphasized</em> text</p>
Copy the code

generates block boxes, and generates inline boxes. Anonymous inline boxes are generated that wrap Some and text, respectively. As shown below:

The data structure

The box model

As mentioned above, the box model consists of content, inside margins, borders, and margins. The content area is a rectangle, and the rest is a margin type that can be set to the top, bottom, left, and right values.

For the content area, define the rectangle structure:

struct Rect {
    var x: Float = 0.0
    var y: Float = 0.0
    var width: Float = 0.0
    var height: Float = 0.0
}
Copy the code

For margin types, it is defined as follows:

// Margin definition
struct EdgeSizes {
    var left: Float = 0.0
    var right: Float = 0.0
    var top: Float = 0.0
    var bottom: Float = 0.0
}
Copy the code

Then the definition of the box model is as follows:

struct Dimensions {
    / / content area
    var content: Rect = Rect()
    
    / / padding
    var padding: EdgeSizes = EdgeSizes()
    
    / / from the outside
    var margin: EdgeSizes = EdgeSizes()
    
    / / frame
    var border: EdgeSizes = EdgeSizes()
}
Copy the code

Then you define the layout type, enumerating, and associating the respective style nodes.

// Layout type
enum BoxType {
    case AnonymousBlock
    case BlockNode(StyleNode)
    case InlineNode(StyleNode)
}
Copy the code

Layout of the tree

The layout tree node contains box model data, layout type, and child nodes. The definition is as follows:

/ / layout tree
class LayoutBox {
    
    // Layout description
    var dimensions: Dimensions
    
    / / type
    var boxType: BoxType
    
    // Child node layout
    var children: [LayoutBox]
}
Copy the code

layout

The layout type of the element is determined by the display attribute, which can be obtained from the node’s stylesheet.

In this case, we only implement the layout of block nodes, leaving the inline ones out for now. Nodes of type block generate block-level boxes, arranged vertically.

So what exactly does layout do? In fact, it is mainly to determine the data in each node box model, such as width, height, margin, and so on.

It is important to have some prior knowledge of how to handle width and height.

The width of a node is determined by the width of the parent node, and is processed from top to bottom. The height of the parent node is determined by the sum of the heights of the children. It is a bottom-up process and the height of the parent node can be determined only after the height of all the child nodes has been calculated.

The whole layout process will be divided into four steps:

  • Width calculation. Calculate the node width and determine the horizontal spacing.
  • Position calculation. Compute node coordinates and determine the vertical spacing.
  • Child node layout. Determine the child node layout information and update the parent node height.
  • Height calculation. Gets the height of the CSS.

Width calculation

Width calculation is the most complicated part. Width, padding, border, margin, and most importantly width and margin.

In addition, width and margin can be set to auto to allow the browser to calculate their values.

  • When width is auto, it means that if margins are available, the parent container width should be set as wide as possible.
  • When margin is auto, the browser selects an appropriate margin.

Suppose we define “width + margin + padding + border” as the total width of the element. As shown below:

There may be a mismatch between the total width and the actual parent container width, either overflow or insufficient. At this time, it is necessary to adjust width and margin according to their respective Settings to meet the requirements.

Now, let’s talk about the adjustment under different circumstances.

1. Do some preliminary work to prepare data. As follows:

  • Extracts the width from the stylesheet. If width is not set, the default isauto.
  • Extract the horizontal margin, left and right values for margin, padding, and border from the stylesheet.
  • To calculate the totalWidth, totalWidth = all margins + width.
  • To calculate the remaining space, leftSpace = parent container width – total width.

2. According to the calculation method in THE CSS (click link 2 at the end of the article for details), it can be divided into the following situations:

  • When width is not auto and the total width is greater than the parent container width, set auto in margin-left/margin-right to 0.

    If margin-left is auto, then margin-left = 0;

    If margin-right is auto, margin-right = 0;

  • When width/margin-left/margin-right are set (i.e., not auto), adjust margin according to block direction.

    If it is LTR, adjust margin-right; If it is RTL, adjust margin-left.

    Here we default to LTR and only adjust margin-right to fit the width requirement.

  • When width is not auto and only one or more of margin-left/margin-right is auto, the auto attribute is set to the remaining space.

  • When width is auto, set the auto attribute in margin-left/margin-right to 0 and width to the remaining space.

  • When width is not auto, margin-left and margin-right are both auto, dividing the remaining space between them.

For specific code handling, see the calculateBlockWidth function in Layout.swift.

3. After processing the above situations, the values of margin and width have been determined, and the box model data can be updated, mainly in the horizontal direction.

Position to calculate

This step is mainly to calculate the coordinate points, and the vertical spacing.

In the vertical direction, take the top and bottom values of border, margin, and padding respectively.

The coordinates of the nodes depend on the location of the parent container. As shown below:

The red dotted box in the figure above is the region of child node 2. Now we need to determine the coordinates of child node 2.

For the x-coordinate, it’s a little bit easier to understand.

Child node x = parent container x + margin + border + paddingCopy the code

The y-coordinate is calculated as follows:

Y of the child node = parent y + parent height + margin + border + paddingCopy the code

This might be a little confusing, but why do I add the height of the parent container?

A block is arranged vertically. Shouldn’t it calculate the space occupied by all the children before that node?

Yes, that’s right. In fact, the height of the parent container is the sum of the heights of the child nodes that have been laid out, as mentioned in the child node layout below.

Child node layout

This step iterates through the child nodes, recursively calculating the layout of the child nodes.

// Compute the child node layout
func layoutBlockChildren() {
        for child in self.children {
            child.layout(containingBlock: self.dimensions)
                        
            // Calculate the overall height
            self.dimensions.content.height += child.dimensions.marginBox().height
        }
  }
Copy the code

Note the calculation of the height of the parent node, which adds up the height of the child node. Each time the layout completes a node, its height is added. Therefore, the height of the parent node is the sum of the heights of the arranged children.

highly

If the height of the node itself is set, the value is taken.

// If height is set, take that value
func calculateBlockHeight() {
    
    if let styleNode = getStyleNode() {
                
        // Get the height of the setting
        if let heightValue = styleNode.getValue(name: "height") {
            
            if case Value.Length(let height, .Px) = heightValue {
                self.dimensions.content.height = height
            }
        }
    }
}
Copy the code

Generate layout tree

Traverses the style tree, generates layout tree nodes according to the layout type of the element, and then generates the layout tree.

// Recursively determine the display data for each node
mutating func buildLayoutBox(styleNode: StyleNode) -> LayoutBox {
    let root = LayoutBox(styleNode: styleNode)

    for child in styleNode.children {
        switch child.getDisplay() {
        
        case .Block:
            let childLayoutBox = buildLayoutBox(styleNode: child)
            root.children.append(childLayoutBox)
            break
            
        case .Inline:
            let childLayoutBox = buildLayoutBox(styleNode: child)
            
            // Inline element, find container
            let container = root.getInlineContainer()
            container.children.append(childLayoutBox)
            break
            
        default:
            break}}return root
}
Copy the code

The above code deals with block and inline elements, respectively. Note here that inline handling has to do with anonymous boxes.

When a block box contains an inline element, an anonymous block box is created to wrap the inline element. Inline elements that line up together are placed in the same anonymous box.

As shown below:

If the last node of the block is already an anonymous box, use it directly; Otherwise create a new box to insert.

// Gets the container for the inline node. If a block contains an inline node, it creates an anonymous block to wrap around the inline
// All inline nodes lined together in a block are placed in an anonymous block for simple processing.
func getInlineContainer() -> LayoutBox {

    switch self.boxType {

    case .AnonymousBlock, .InlineNode(_):
        return self
        
    case .BlockNode(_):
        // Fetch the last child node
        let lastChild = self.children.last
        
        // If it is already an anonymous box, do not process it and return later
        if case.AnonymousBlock = lastChild? .boxType { }else {
            // Generate a new anonymous box
            let anonymousBlock = LayoutBox(boxType: .AnonymousBlock)
            
            // Add anonymous anonymous boxes
            self.children.append(anonymousBlock)
        }
        
        // Returns the last child node
        returnself.children.last! }}Copy the code

Finally, the layout tree is laid out to determine the node location information.

The full code can be viewed at: github.com/silan-liu/t…

conclusion

This section describes how to determine the layout information of the nodes and generate a layout tree. The most complicated one is the calculation of the width, which is a bit too much to handle.

The next section will show you how to draw and convert layout information into pixels. Stay tuned

The last

If you think the article is helpful, you can click on the following business card to pay attention to the public number “smiling snail”.

Reply “snail” in the chat box of the public number, you can add wechat for communication ~

The resources

  • www.w3.org/TR/CSS21/vi…
  • www.w3.org/TR/CSS2/vis…
  • Limpet.net/mbrubeck/20…