Refactoring BlockLayout
Earlier today, I pushed a major change to Chapter 5 of Web Browser Engineering, unifying the BlockLayout
and InlineLayout
classes. Since we published Chapter 5 almost two years ago, I wanted to explain the change and why Chris and I made it.
How Layout Works in WBE
Chapter 5 is about browser layout: building the layout tree, computing sizes and positions of the layout nodes, and using that information to paint the web page to the screen.
Until this change, the layout tree had two key types of nodes: BlockLayout
and InlineLayout
. BlockLayout
nodes were used for things like <div>
or <body>
, elements that contain a sequence of vertically stacked blocks, while InlineLayout
nodes were used for things like <p>
or <h1>
, which contain text laid out left to right in lines.
There are basically two key things a layout node needs to be able to do: construct its children, and compute its own size and position.1 BlockLayout and InlineLayout construct their children differently (BlockLayout
creates BlockLayout
s and InlineLayout
s; InlineLayout
s create LineLayout
s and TextLayout
s) but compute their sizes and positions more or less identically.2 So it was kind of a judgement call whether these should be one class, thereby sharing the size and position computation but with ugly conditionals for the tree construction, or two classes, avoiding the conditionals at the cost of some duplication.
When we were first writing Chapter 5, it looked like splitting the two classes lead to cleaner code overall, so we went with that.
Why Change?
First of all, since we wrote Chapter 5, we’ve written a lot more chapters, and that’s made the costs and duplication versus conditionals much clearer. Chapters 11 and 13 about the graphics pipeline, for example, required lots of changes to painting for BlockLayout
and InlineLayout
. Typically, those changes have to be duplicated across both classes (ugly!), and it’s pretty easy to forget change both in the same way, meaning that it’s easy to end up with rare bugs or to have to fix the same bug twice. That’s frustrating.
Second, I’ve taught the material in the book a couple of times, and I’ve found that it’s hard for students to keep in mind the distinction between BlockLayout
and InlineLayout
. It’s not that it’s hard to differentiate between paragraphs of text and a section containing paragraphs; it’s just that a paragraph is still a block, so if you’re implementing, say, background images, it seems natural to do that in BlockLayout
, even if your test page uses a paragraph. That leads to false starts and confusions. It doesn’t help, by the way, that CSS has the concept of display: block
and display: inline
, and those don’t correspond to BlockLayout
versus InlineLayout
(the book doesn’t implement the display
property).
Third, and ultimately what was decisive, is the ongoing work on invalidation in Chapter 16. Without writing a whole chapter here on what invalidation is and how it works, one important step is caching the layout tree instead of recreating it every time the browser does layout. This requires only re-creating a layout node if its dependencies change. But this creates a problem with the split between InlineLayout
and BlockLayout
: it is a node’s parent that creates the InlineLayout
or BlockLayout
, so the node’s parent needs to keep track of changes to its children to recreate them as the correct layout node type. This was very hard to explain, in what’s already going to prove a very complicated chapter, and would be avoided entirely by unifying the InlineLayout
and BlockLayout
classes.
Making the Change
I know many readers are following along with the book, implementing their own web browsers and sometimes even going way beyond the book. It’s up to you whether you want to update your browser with the unified BlockLayout
, but you may want to, and if you do it’ll probably be easier to incorporate Chapter 16 when it is eventually published.
I think the change won’t be too bad to incorporate. I recommend doing it in a few steps.
First, track down where InlineLayout
s are created in your code. Most likely, they are only created by BlockLayout
’s layout method, inside the tree creation for
loop. Remove that code; once we unify BlockLayout
and InlineLayout
, that for
loop will only create BlockLayout
s.
Second, copy all the InlineLayout
methods into BlockLayout
. That includes recurse
, text
, input
, new_line
, and get_font
.
Third, verify that the paint
method and the constructor are the same for InlineLayout
or BlockLayout
. It’s possible that they’re different—maybe only InlineLayout
has backgrounds, or only BlockLayout
supports visual effects. In that case, you’ll want to merge the two paint
methods. It should be possible to support all the paint features that either supports.
At this point the only thing left unmerged should be the layout
methods of the two classes. These two methods should have a lot of duplication—they should compute sizes and positions in the same way, and both should recursively call layout
on their children—but they will have different tree creation code. You’ll want to use a conditional to unify the tree creation code, like this:
class BlockLayout:
def layout(self, zoom):
# ...
mode = layout_mode(self.node)
if mode == "block":
# BlockLayout tree creation code
else:
# InlineLayout tree creation code
# ...
The BlockLayout
tree creation code here is a for
loop, while the InlineLayout
tree creation code calls new_line
followed by recurse
.
Once you’ve made these changes, verify that your browser works, that InlineLayout
has no references in your code, and that all its methods have been copied into BlockLayout
, and then delete the InlineLayout
class.
Future Changes
As much as I would love to be “done” with the already-published chapters of WBE, we’ll probably need to continue pushing fixes or changing the code of earlier chapters as we work on later ones. Once we reach the end of the book, we’ll probably do a clean-up pass as well. So there are probably more book-wide changes coming up. So far those changes have been minor (changing something from a local variable to a field, for example) and we’ve pushed the changes without announcement, but if we need any more major changes we’ll put up a blog post.
But don’t let these changes keep you from hacking away on your own browser. One of the goals of this book is to give you ownership of a web browser. If you don’t like our changes, it’s your browser and your code! Or you may well have found your own workarounds to these problems, or written enough code on the old abstractions that it’s hard to port. That, too, is part of working on web browsers, and as good a reason as any to skip the updates. Happy hacking!
Both happen in the layout
method in WBE. In a real browser, these two steps are often distinct—there may even be some intermediate steps—but I decided against making them separate steps in WBE because it would require going into the details of multi-phase layout when handling line breaking.
In Chapter 5, the computation of height is actually different for BlockLayout
and InlineLayout
, but by Chapter 7, when LineLayout
is introduced, they become identical.