Painting: From Layout Boxes to a Display List

Building a Web Browser Engine: Layout & Rendering

Chapter 7 · Painting: From Layout Boxes to a Display List

Every chapter so far has computed real numbers — widths, positions, line breaks — but none of it has said anything about what actually gets drawn. This chapter builds the bridge: walking a finished layout tree and producing a real, flat, ordered display list of simple paint commands — the exact intermediate representation real browsers build before ever touching a pixel.

A Paint Command, and the Simplest Real Thing to Paint

class PaintCommand: def __init__(self, kind, **kwargs): self.kind = kind self.__dict__.update(kwargs) def paint_background(layout_box, display_list): style = box_style(layout_box) bg = style.get('background-color', 'transparent') if bg != 'transparent' and layout_box.dimensions is not None: rect = layout_box.dimensions.border_box() # background extends to the BORDER edge display_list.append(PaintCommand('rect', rect=rect, color=bg))

A background genuinely paints out to the border box, not the content box — real CSS behavior, and exactly why border_box() (Chapter 2) is what gets used here, not content directly.

A Naive Walk Order — and the Real Visual Bug It Causes

def build_display_list_naive(layout_box, display_list=None): if display_list is None: display_list = [] for child in layout_box.children: build_display_list_naive(child, display_list) # children FIRST paint_background(layout_box, display_list) # then this box's own background return display_list

Looks harmless — walk the tree, collect commands. But a display list is painted in order: each command draws on top of everything already painted before it, exactly the way a real rasterizer works.

Verified directly — a child's background gets completely painted over by its own parent
A real, laid-out nested box (via the actual Chapter 3/4 layout_block pipeline, not hand-faked rectangles): box A (background: red, filling a 300×50 area) containing box B (background: blue, 100×50, sitting fully inside A). The naive walk produces the display list ['blue', 'red']B's command comes first, A's comes after. Simulating what's actually visible at a point sitting inside both boxes, (50, 25): the answer comes out red. A's own background, painted last, completely covers B's — a real browser would show B's blue box sitting visibly on top of A's red one, not swallowed by it.
def build_display_list(layout_box, display_list=None): if display_list is None: display_list = [] paint_background(layout_box, display_list) # THIS box's own background first for child in layout_box.children: build_display_list(child, display_list) # then recurse into children return display_list
Verified directly — swapping the order fixes it, and a sanity check confirms the fix is scoped correctly
Same two boxes, fixed order: display list ['red', 'blue']. The same overlap point now correctly resolves to blueB visibly sits on top of A, exactly matching real CSS stacking (a child paints in front of its own parent). A point inside A but genuinely outside B's own bounds, (250, 25), resolves to red in both the naive and fixed versions — confirming the bug specifically affects the overlap region, not areas where only the parent's background was ever going to be visible anyway.

Where This Connects

This chapter's findingWhat it connects to
A flat, ordered display list, painted strictly in sequenceChapter 8's own software rasterizer — this exact display list is the direct input; the rasterizer's own job is simply to execute each command in order onto a real pixel buffer
Background painting out to the border edge, using border_box()Chapter 2's own box-model machinery, reused unchanged — this chapter needed no new geometry code at all, only a new way of walking the tree that was already fully built
Paint order matching nesting depth, verified at three levels deepChapter 9's own stacking and paint-order chapter, which will need to handle real CSS cases (like z-index) where paint order and DOM nesting order genuinely diverge — this chapter's own "parent first, then children" rule is the honest, simpler default that real CSS uses whenever nothing overrides it

Hands-On Exercises

Exercise 1

Build a container with a single child whose own background-color is left at the default transparent. Run it through build_display_list() and confirm the resulting list is completely empty — not a command with a transparent color, genuinely no command at all — then explain which specific line in paint_background() is responsible.

📄 View solution
Exercise 2

Build a genuinely three-level-deep nested layout — an outer box, a middle box inside it, and an inner box inside the middle one — each with its own distinct background color. Run it through build_display_list() and confirm the resulting paint order matches the nesting depth exactly: outermost first, innermost last.

📄 View solution
Exercise 3

Reproduce this chapter's own two-box bug directly: lay out box A (background:red) containing box B (background:blue) using the real layout pipeline, then build display lists with both build_display_list_naive() and build_display_list(). Compute color_at_point() for a point inside both boxes and confirm the two versions disagree. Identify the exact two-line reordering responsible for the difference.

📄 View solution

Chapter 7 Quick Reference

  • Display list: a flat, ordered sequence of simple paint commands, built by walking the finished layout tree
  • Background painting: paints to the border box, not the content box — reuses Chapter 2's own border_box() unchanged
  • Real bug found and fixed: recursing into children before appending a box's own background command puts the parent's background LATER (painted on top) — verified completely hiding a real child's background wherever the two overlap
  • The fix: paint a box's own background first, then recurse — matching real CSS paint order (parent, then children, outermost to innermost)
  • Verified: the fix holds at three levels of nesting, not just two, and a transparent background genuinely produces no command at all
  • Next chapter: A Software Rasterizer — turning this display list into an actual pixel buffer, verified pixel by pixel