Gecko:How Scrolling Works

Revision as of 08:33, 3 January 2005 by Callek (talk | contribs) (make the link to ns[Gecko:XULScroll Frame] actually read as "nsXULScrollFrame" to flow with text better.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Scrolling is rather complicated. Suppose you have an HTML element X with 'overflow:scroll' which has HTML children. We create anonymous content like this:

 <X>
  |
  +--- regular X children
  |
  +--- <scrollbar> (horizontal scrollbar)
  |
  +--- <scrollbar> (vertical scrollbar)
  |
  +--- <scrollcorner>

For various values of 'overflow' the horizontal, vertical or scrollcorner content may not actually be created if we know they'll never be needed.

XBL bindings in scrollbar.xml then create anonymous content under these elements. For example scrollbars usually get a XUL slider, a thumb, and up and down buttons.

Then we usually have a frame tree that looks like this:

 nsGfxScrollFrame (XUL scroll frame)
  |
  +--- nsScrollPortFrame (XUL scrollport, which is an nsScrollBoxFrame)
  |     |
  |     +--- nsBlockFrame (scrolled frame)
  |
  +--- nsScrollbarFrame (XUL horizontal scrollbar)
  |
  +--- nsScrollbarFrame (XUL vertical scrollbar)
  |
  +--- nsBoxFrame (XUL corner)

The nsBlockFrame is basically the frame we would have constructed for X without scrollbars.

Styling is tricky. We want border, outline and most other styles set on X to apply to the nsGfxScrollFrame not to the nsBlockFrame or any of the scrollbar frames. But if someone applies "inherit" to a child of X, that needs to work. So we put X's style context on the nsGfxScrollFrame and make the style contexts for the scrollport frame, the scrolled frame, the scrollbar frames and the corner frame all be "-moz-scrolled-content" pseudo-element child contexts of that. The style contexts for the children of the scrolled frame are made direct children of the style context on the scroll frame, skipping over the scrolled frame and scrollport frame style contexts.

Because the scrollport is a XUL frame and the scrolled frame is usually an HTML frame, XUL creates an nsBoxToBlockAdaptor box object between the scrollport and the scrolled frame. So reflow of the nsGfxScrollFrame works like this:

  • nsGfxScrollFrame::Reflow is redirected, as for all XUL frames, to the XUL layout mechanism.
  • XUL layout gets the min, max and preferred sizes of the children
    • The scrollport gets its sizes by asking the boxtoblock adaptor
    • The adaptor gets the sizes by calling Reflow on the block frame
  • nsGfxScrollFrame::DoLayout uses that information, plus taking into consideration whether or not scrollbars are needed (which is passed on whether we have enough space for the preferred size of the scrollport), to size and position its children.

Unfortunately during the trip through XUL some important information is lost, like what the available width of the containing block is. It is tricky to transmit that information accurately through to the block adaptor. Sometimes it's practically impossible.

After reflow the scrollbars and scrollcorner are set to zero width or height if they are not needed. The scrollport occupies the rest of the scroll frame's area. The nsBlockFrame is always sized to its desired height and positioned at the top left corner of the scrollport.

The actual scrolling presentation is done by views, in particular nsScrollingView. The only two frames which *must* have views are the scrollport and the scrolled frame. (The scroll frame and the scrollbars may have views for unrelated reasons of their own, e.g. if they're translucent.) The scrollport frame is an nsScrollingView. The scrolled frame view is a normal view. The nsScrollingView clips its children. It also positions its child view at a negative offset so that the child appears to be scrolled down. This is the only situation in which the offset of a view V1 from its parent V2 is not equal to the offset of V1's frame from V2's frame.

Actually causing a scrolling motion is a complicated dance. Scrolling can be triggered in a couple of important ways:

  • Code such as a C++ event handler calls into nsIScrollableView to force scrolling to happen
  • Someone sets the curpos attribute of a scrollbar attached to an nsGfxScrollFrame. This is the case when the user interacts with a XUL scrollbar, but it can also be triggered from script. The scrollbar frame can set its own curpos when reflow increases the size of the scroll area so that more of the scrolled frame fits on the screen and we need to scroll left or up to avoid showing garbage.

Suppose someone calls into nsIScrollableView to force scrolling. The view scrolls by changing the child view offset and, if there is a widget, asking the widget to perform the visual scroll and invalidate the uncovered area, or else just repainting the entire area. If smooth scrolling is enabled then the view doesn't scroll immediately but schedules a series of small-step scroll events to be processed over time. Whenever an actual scroll motion happens, the view issues a callback to registered nsIScrollPositionListener objects. The most important such listener is the nsGfxScrollFrame associated with the scrollport. That listener updates the 'curpos' attributes on the horizontal and vertical scrollbars. This causes attribute change notifications to be received by the scrollbar frames. They call back to the nsGfxScrollFrame to signal that the position has changed. nsGfxScrollFrame then tells the scrollbars to update themselves.

The case where someone sets a curpos attribute on a scrollbar uses most of the same code paths as above but in a different order. The attribute change notifications fire and we go to nsGfxScrollFrame. nsGfxScrollFrame detects that this attribute change was *not* triggered by the view and therefore asks the view to scroll. Eventually the view calls back into the listener again and that's where the scrollbars are actually updated.

Communicating between scrollbars and the rest of the machinery by setting attributes really sucks, because sometimes it has to happen during reflow and setting attributes during reflow is very very bad.

Note that XUL <scrollbox> objects correspond to a naked nsScrollBoxFrame scrollport, one that has no nsGfxScrollFrame.

An element that supports scrollbars can fire DOM events to indicate when content starts overflowing or when it no longer overflows. This is handled by the scrollport frame. The scrollport frame also handles saving and restoring the scroll position.


Future plans:

  • We need to avoid going through XUL for HTML-inside-HTML, which is the common case on the Web, of course. So we are currently forking nsGfxScrollFrame into nsHTMLScrollFrame and nsXULScrollFrame. The latter is the scrolling container for XUL elements and the former is the scrolling container for all other elements. They share functionality by embedding an nsGfxScrollFrameInner object.
  • nsHTMLScrollFrame doesn't want the scrollport frame to be a XUL box, because that would defeat the purpose. In fact we'd rather just manage the scrolled frame directly.
  • Suggestion: let's get rid of the scrollport frame entirely. Eliminate the nsScrollBoxFrame/nsScrollPortFrame classes. The functionality currently on the scrollport frame can be moved up to the scroll frames (and shared in the nsGfxScrollFrameInner class). The nsScrollingView will be an anonymous child view of the scroll frame (just like nsSubDocumentFrame has an anonymous child view inside it). When the scroll frame is created to wrap the scrolled frame, the view for the scrolled frame is reparented to the nsScrollingView (or created with the correct view, if it doesn't already exist). The scrolled frame is pulled up to become a direct child of the scrollframe.
  • This will break XUL <scrollbox> since it alone has a scrollport with no scroll frame. But I think we can fix this by just making a <scrollbox> be a regular box with 'overflow:hidden', giving it a regular scroll frame. We will need to create an nsScrollBoxObject on this box though.

Other plans:

  • We need to fix the way scroll notifications work. Scrollbars attached to scrollframes should not support curpos, maxpos, pageincrement or increment attributes. The scrollbar and slider frame code should obtain these values by querying nsIScrollableFrame directly. Where scrollbars currently set curpos, to correct for situations where the viewed area would extend beyond the scrolled frame, this should simply be handled by the scroll frame. When scrollbars currently set curpos in response to a scroll operation, we instead call back to the scroll frame to inform it of the scroll request. Scrollbars not attached to scrollframes can continue to read and write their attributes directly.