Redoing the browser chrome
Over the past few months, as Chris and I worked to finalize a Web Browser Engineering manuscript for publication, we made several major changes to how the toy browser chrome is drawn. The changes simplify it a bit and make it more similar to how real web browsers work, at the cost of a bit more conceptual complexity. I want to explain the changes.
The old way
The browser chrome is a little complicated because it works one way before Chapter 11 and another way in Chapter 11 and onward, though there are similarities. Before Chapter 11, the browser chrome was drawn as part of the Browser
’s draw
method by directly invoking methods on the Tk Canvas
object that the browser is drawn to. The arguments to those calls were just hard-coded numbers. This had the benefit of conceptual simplicity—the chrome is drawn “raw”, just method calls and constants.
After Chapter 11 switched to Skia that “raw”ness became hard to maintain, because the Skia API is more verbose, so instead the chrome code made calls to wrapper functions like draw_line
and similar. One benefit of this old approach was that we didn’t need to define DrawLine
and DrawOutline
classes until much later in the book, since when the chrome was first implemented page content only contained rectangles and text.
But I didn’t like the draw_line
functions—it felt weird to have a DrawLine
class that calls a draw_line
function—and also using “raw” APIs meant that the transition to Skia required a lot of quite tedious porting from one API to another. That porting was annoying enough that the book left it to the reader, which made it harder for readers to make it through Chapter 11. Ultimately, the “raw” API approach, while it had its benefits, just introduced needless complexity to the Skia port, and that wasn’t worth it.
The new way
The new way avoids all of these issues by first painting the browser chrome to a display list, which is then drawn to the Canvas
or Surface
just like page content. This does mean that we need the DrawLine
and DrawOutline
paint commands earlier, as soon as they’re used in the browser chrome, and it also means we have to accept some minor hits to aesthetics, like the back button now using the less-than character instead of a manually painted triangle to represent the back arrow. But using the same paint commands for page content and browser chrome means that nothing major has to be ported to Skia, and the DrawLine
command is also useful for cursor drawing in Chapter 8.1
As we were doing this, we made another big change to the browser chrome: we added layout. You see, previously we gave exact pixel coordinates for the corner of every button and text field, and that worked fine because the browser chrome was basically static. But it caused problems across different operating systems, because different operating systems use different fonts of different sizes. The book asked readers to adjust the coordinates for their system, but actually observing some readers made it clear that that was difficult, and most readers weren’t learning much from doing it.
So now the browser chrome uses font measurements and metrics to determine the position and size of each browser chrome element. This means it’ll look right on every operating system from the start, and also reinforce layout reasoning, which is a big part of the book. One downside of this change is that hit testing for the browser chrome used to be trivial, since the size and position of each button and input field was hard-coded. After this change, the layout depends on font metrics, so we chose to compute the browser chrome layout in a layout phase and then retain that layout for later paint and hit-test phases. In fact, the browser chrome ends up structured very similarly to ordinary layout objects,2 further emphasizing that browser chrome and page content are more alike than not.3
The advantages
Basically, this change made Chapter 11, the port to Skia, a lot less laborious, and while it added a lot of conceptual complexity—a chrome display list, a Chrome class, a hit-testing method—a lot of that conceptual complexity is already familiar from layout objects and seeing it again might actually help readers review how layout and paint works and why both phases are needed.
The new changes are live on the site, so do check them out and let us know (by commenting below or by emailing us) whether you like them.
Before, the text cursor was also drawn “raw” up until Chapter 13. (Chapter 13 adds transforms, which require the cursor to be part of the display list so that it can be transformed properly.) Now, the text cursor is part of the display list from its introduction in Chapter 8, because there’s now already a DrawLine
command at that point. This makes the code less buggy and removes a refactor later on.
Ordinary layout objects don’t have a hit-test method in our toy browser, so that’s one difference. I do think having each layout object type hit-test itself would be a clean design, but it would add a lot of duplicative code, so we decided against doing that.
And this is in fact more like how modern browsers work. Chrome, for example, has a custom UI library for implementing the browser chrome, much like our browser. Firefox goes a step further and today uses the web renderer directly, though historically it used this thing called XUL which was sort-of the web renderer but sort-of not.