Gecko:Reflow Refactoring

From MozillaWiki
Jump to navigation Jump to search

Summary

Reflow refactoring is the name for two related changes currently being worked on mainly by dbaron:

  • Change intrinsic width computation so that it happens in separate methods on nsIFrame. (Currently preferred with calculation using the nsIFrame API can happen in two different ways as well: having only one codepath reduces incremental layout ("{inc}") bugs.)
  • Change the way we handle incremental layout to use dirty bits on the frames rather than the reflow tree (which is essentially out-of-line dirty bits) and a complicated system of reflow reasons that often leads to weird incremental layout ("{inc}") bugs.

The goals of these changes are:

  • simplification of code
  • fixing incremental reflow ("{inc}") bugs
  • fixing intrinsic sizing bugs
  • allowing better integration of nsIBox and nsIFrame layout
  • allowing easier implementation of new features like 'inline-block'

Quick Links

Build Instructions

Follow the normal build instructions and get the source from CVS, except when checking out client.mk, use

 cvs -d :pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot checkout -r REFLOW_20050804_BRANCH mozilla/client.mk

Using client.mk to pull the rest of the tree is essential because only layout is branched; everything other than layout is pulled by date.

Rationale

The goal of the branch is to change the design in two areas of layout that have given us the most problems due to their complexity. It will also help make the nsIFrame and nsIBox layout methods a little more similar.

nsIFrame and nsIBox layout

Understanding the current situation with Mozilla's rendering objects requires understanding a bit of history, since it's currently in a state that nobody really intended.

CSS sort of has a notion of a rendering tree: a tree structure that's similar to the content tree but also contains pseudo-elements and does not contain objects that are display:none. In the original Gecko codebase, the objects in the rendering tree were intended to be implementations of nsIFrame. Different implementations would be used for different types of rendering objects: for example, objects with CSS display:block (nsBlockFrame), objects with CSS display:inline (nsInlineFrame), HTML <input type="text"> or <textarea> (nsTextControlFrame), or images resulting from <img> or <object> (nsImageFrame).

When the XUL layout model was implemented, nsIFrame was found not to be suitable for the flexible box model needed for XUL. So, for better or for worse, a second interface for rendering objects came into existence: nsIBox. XUL layout objects also delegate to singleton nsIBoxLayout objects for a number of the things that differentiate different types of rendering objects.

In an effort to reduce the size of box rendering objects (which had to implement nsIFrame and nsIBox), the two rendering object interfaces have been merged into nsIFrame and the merged list of methods is on track to be gradually consolidated. Base implementations of the separate methods are still split between nsBox and nsFrame, although the latter inherits from the former. The method nsIFrame::IsBoxFrame is the way the two types of objects can currently be distinguished.

The handling of the boundary between box and non-box rendering objects is currently handled in two places: the code in nsBoxFrame and nsLeafBoxFrame handles a box inside a non-box. The box methods on nsFrame handle a non-box being inside a box (although these used to be on a separate class called nsBoxToBlockAdaptor).

Intrinsic sizing changes

One of the messy areas in the current code is the way we do intrinsic width calculation. A number of aspects of CSS and HTML layout (such as table layout and floats or absolutely positioned elements with width:auto) depend on two different concepts of intrinsic width. One of those we generally call the preferred width: this is roughly the widest width that the element can usefully occupy. For example, the preferred width of a line of text is the width of that line when laid out without any breaks. The second we generally call the minimum width: this is roughly the narrowest width that the element can occupy without overflow. For example, the minimum width of a line of text is the width of the widest word in the line. dbaron has started to write a specification for intrinsic widths.

The basic problem with what's messy about intrinsic sizing in nsIFrame layout in Gecko is that a single method, nsIFrame::Reflow, is used to do final layout and to calculate both types of intrinsic widths. The minimum width is calculated by asking for the "max-element-width", and the preferred width is calculated in one of two ways: reflowing with NS_UNCONSTRAINEDSIZE as the available width and computed width or setting the NS_REFLOW_CALC_MAX_WIDTH flag. There are also block flags that tell blocks to compute their final size in special ways that depend on the intrinsic widths in various ways.

The other problem is that box layout uses a completely different intrinsic sizing mechanism: it uses three intrinsic widths and intrinsic heights: GetMinSize, GetPrefSize, and GetMaxSize (which isn't generally all that interesting or useful). These are separate methods from the box method to do final layout, which is called Layout. Note that the size (width x height) vs. width difference between the two layout models will remain after this work.

Incremental layout Changes

Purpose of incremental layout

(This section is taken from a post dbaron wrote.)

There are certain situations where it's important that we not recompute the layout of everything in the page. (These are in general based on the idea that layout time is roughly linear in the amount of content being laid out.)

  1. Ensuring that the time to respond to small changes is proportionately small, which is important for:
    1. incremental loading of pages
    2. specified style changes to very small parts of pages
    3. editing operations
  2. resizing of the window
  3. changes to 'top', 'left', 'right', and 'bottom'

Pretty much everything else doesn't matter. In particular, any changes to the specified style for an element should mean *everything* inside that element gets recomputed (and a good bit outside, but that falls under the optimizations for (1)).

The optimization to make in (2) is not to recalculate intrinsic widths (min/max widths), which is much simpler to do in a world where min/max width computation were separate from reflow. (This is one of the connections between these two changes that makes them easier to do at the same time.)

It would be nice if the optimization we'd need to make for (3) meant we could just move things and recompute overflow areas. I think this was the original intent of those properties. I also thought this when I commented in bug 157681. However, I'm afraid that's not actually the case, and the rules in the current CSS2.1 draft are correct as far as WinIE compatibility goes. However, caching of preferred and minimum widths should mean that we don't need to re-layout in the vast majority of cases.

The optimizations necessary for (1), particularly (1a), should maintain the invariants that:

  1. The page always ends up displayed the same way, no matter what the units of incremental layout were during its load
  2. The result after any incremental load of part of a page should be the same as if that part were the whole page (excluding unloaded images, etc.) Otherwise we confuse authors and encourage hacks to prevent incremental reflow.

Incremental loading of content requires recomputation of preferred widths and of sizes for almost any content that has descendants modified. More on this later, perhaps...

The current incremental layout system

The current incremental layout system used by non-box layout is quite complex and has been the source of hard-to-fix bugs (although we've worked around most of the serious ones). The basic idea is that there are a bunch of different types of reflows that can be done, and different types cause different things to be recomputed. We have two different enums for these: reflow types (stored on reflow commands) and reflow reasons (passed as input to nsIFrame::Reflow within the nsHTMLReflowState). When a frame is dirty for some reason, we create a reflow command object (nsHTMLReflowCommand) which is put in a reflow tree (of nsReflowPath), which is used to track which frames have dirty children and coalesce the processing of multiple reflow commands into a single reflow.

One of the reflow reasons is eReflowReason_Incremental, which means that there is at least one reflow command on a descendant but otherwise nothing needs to be handled. Another is eReflowReason_Resize, which means that only the final width needs to be recalculated (intrinsic widths do not). Another is eReflowReason_StyleChange, which means that everything, including intrinsic widths, needs to be recalculated. The other reflow reasons and the reflow types not corresponding to these reasons (other than incremental, which has no corresponding reflow type) are poorly defined and hopefully unnecessary.

Explain bits (both for box and non-box)...

Design

Intrinsic Sizing

To be written...

Interesting issues:

  • content box vs. border box

Incremental Layout

To be written...

Interesting issues:

Status

Currently a lot of the design is done but subject to change. CSS block/inline layout code and XUL box layout code have been converted to the new design and work well enough that the Firefox UI basically works (although with some glitches). However, form controls and tables have not yet been converted to the new design, and are disabled on the branch.

Future Development Tasks

  • fix intrinsic sizing bug in default browser prompt (dbaron priority #1)
  • convert table code (dbaron priority #2)
  • convert form control code
    • figure out whether to refactor nsLeafFrame::GetDesiredSize and implement nsLeafFrame::GetMinWidth and nsLeafFrame::GetPrefWidth in terms of it or whether to implement GetMinWidth and GetPrefWidth separately for all the subclasses of nsLeafFrame that use GetDesiredSize
  • Figure out whether the inline min/pref width interfaces currently on the branch (nsIFrame::GetInlineMinWidth and GetInlinePrefWidth) are sufficient or whether we need to make additional architectural changes related to continuations (particularly making various special things use the existing next-in-flow model)

Future Testing Tasks

  • Make sure dbaron's existing testcases work
  • Develop testcases that test box-block boundaries better
  • Extend intrinsic sizing testcases to check that padding, border, and margin are counted correctly, especially around box-block boundaries
  • Test XUL user interfaces
  • Test Web pages