Gecko:How Scrolling Works

From MozillaWiki
Jump to: navigation, search

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

  +--- 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:

  +--- 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 nsHTMLScrollFrame 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 nsHTMLScrollFrame 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.

The actual scrolling presentation is done by views, in particular nsScrollPortView. The nsHTMLScrollFrame and the scrolled frame must have regular views. The nsHTMLScrollFrame maintains an anonymous inner view, the nsScrollPortView, which is a child of the scrollframe's view and the parent of the scrolled frame's 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 nsHTMLScrollFrame. 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 nsHTMLScrollFrame 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 nsHTMLScrollFrame to signal that the position has changed. nsHTMLScrollFrame 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 nsHTMLScrollFrame. nsHTMLScrollFrame 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.

These following work items have all been completed in 1.8!

  • 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.
  • <strike>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.