Source/core/page/scrolling directory contains utilities and classes for scrolling that don‘t belong anywhere else. For example, the majority of document.rootScroller’s implementation as well as overscroll and some scroll customization types live here.
document.rootScroller is a proposal to allow any scroller to become the root scroller. Today‘s web endows the root element (i.e. the documentElement) with special powers (such as hiding the URL bar). This proposal would give authors more flexibility in structuring their apps by explaining where the documentElement’s specialness comes from and allowing the author to assign it to an arbitrary scroller. See the explainer doc for a quick overview.
There are three notions of
rootScroller: the document rootScroller, the effective rootScroller, and the global rootScroller.
Elementthat's been set by the page to
document.rootScroller. This is
Nodethat‘s currently being used as the scrolling root in a given frame. Each iframe will have an effective rootScroller and this must always be a valid
Node. If the document rootScroller is valid (i.e. is a scrolling box, fills viewport, etc.), then it will be promoted to the effective rootScroller after layout in a lifecycle update. If there is no document rootScroller, or there is and it’s invalid, we reset to a default effective rootScroller. The default is to use the document
Node(hence, why the effective rootScroller is a
Noderather than an
Nodeon a page that's responsible for root scroller actions such as hiding/showing the URL bar, producing overscroll glow, etc. It is determined by starting from the root frame and following the chain of effective rootScrollers through any iframes. That is, if the effective rootScroller is an iframe, use its effective rootScroller instead.
Each Document owns a RootScrollerController that manages the document rootScroller and promotion to effective rootScroller. The Page owns a TopDocumentRootScrollerController that manages the lifecycle of the global rootScroller.
Promotion to an effective rootScroller happens as a step in the main document lifecycle. After all frames have finished layout, we perform a frame tree walk (in post-order), performing selection of an effective rootScroller in each frame. Each time an effective rootScroller is changed, the global rootScroller is recalculated. Because the global rootScroller can affect compositing, all of this must happen before the compositing stage of the lifecycle.
Once a Node has been set as the global rootScroller, it will scroll as though it is the viewport. This is done by setting the ViewportScrollCallback as its applyScroll callback. The global rootScroller will always have the ViewportScrollCallback set as its applyScroll.
The rootScroller can be composed across documents. For example, suppose we have the following document structure:
+--------------------------------------+ | Root Document | | +-----------------------------+ | | | iFrame | | | | +------------------+ | | | | | <scrolling div> | | | | | +------------------+ | | | +-----------------------------+ | +--------------------------------------+
We want to make the scrolling div be the global rootScroller (scrolling it should move the URL bar).
Iframes are special when computing rootScrollers, they delegate computation of the global rootScroller to the effective rootScroller of their content document. In the above case, we'd need to set:
rootDocument.rootScroller = iframe; iframeDocument.rootScroller = scrollingDiv;
If both the iframe and scrollingDiv are valid effective rootScrollers then the scrolling div will become the global rootScroller.
One complication here has to do with clipping. Since the iframe must be a valid rootScroller, it must have
width: 100%; height: 100%. This means that when the URL bar is hidden, the iframe wont fill the viewport and so it‘ll normally clip the contents of the global rootScroller (
height 100% is statically sized to fill the viewport when the URL bar is showing). We compensate for this by making the iframe geometry match its parent frame when it is the effective rootScroller. We also set the layout size to match the parent’s layout size so that we preserve the URL bar semantics of the root frame WRT layout. i.e.
100vh is larger than
height: 100% by the size of the URL bar.
We also disabling clipping (
SetMasksToBounds(false)) on any iframes that have the global RootScroller as a descendant during the compositing stage of the document lifecycle. This prevents clipping during URL bar movement, before Blink receives a resize.
position: fixed Elements are statically positioned relative to any viewport scrolling. Generally, they‘ll be positioned relative to the viewport container layer so they don’t move with scrolling.
When the URL bar is shown or hidden, the viewport will resize and so any bottom-fixed layers will need move to account for the newly exposed or hidden space (since they‘re positioned relative to the layer top). i.e. They appear to stay fixed to the bottom of the device viewport. However, this is all done from Blink when a Resize message is received. For URL bar resizes, a resize message is sent to Blink only once the user has lifted their finger from the screen. While the user has their finger down and is scrolling, moving the URL bar, Blink isn’t notified of these changes. Since the layers are positioned relative to their container's origin (the top), if we want position: bottom layers to appear fixed to the bottom during the scroll we need to adjust their position dynamically on the compositor.
This adjustment happens in CC in LayerTreeHostImpl::UpdateViewportContainerSizes. The inner and outer viewports have their bounds modified by setting a “bounds delta”. This delta is non-0 only while the URL bar is being moved, before the Resize is sent to Blink, and accounts for changes that Blink doesn‘t know about yet. Once Blink gets the resize, the layers’ true bounds are updated and the delta is cleared. The bounds_delta is used by the TransformTree to create a transform node that will translate bottom-fixed layers to compensate for the URL bar resize before Blink has a chance to resize and reposition them.
For a non-defualt document.rootScroller, we want position: fixed layers to behave the same way. When we set a global rootScroller, Blink sets it as CC‘s outer viewport. This means that we get the container resize and position: fixed behaviors for free. However, we also want position: fixed Elements in parent frames to also behave as if they’re fixed to the viewport. For example, a page may want to embed an iframe with some content that‘s semantically the rootScroller. However, it may want to add a position: fixed banner at the bottom of the page. It can’t insert it into the iframe because it may be cross-origin but we still want it to stay fixed to the bottom of the viewport, not the iframe. To solve this, all rootScroller‘s ancestor frames are marked with a special bit, is_resized_by_browser_controls. The transform tree construction relative to the outer viewport’s bounds_delta mentioned above will apply to any layer marked with this bit. This makes position: fixed layers for all rootScroller ancestors get translated by the URL bar delta.
When the ImplicitRootScrollerEnabled setting is enabled, Blink may choose an element on the page to make the effective root scroller, without input from the page itself. This attempts to provide a better experience on existing pages that use an iframe or div to scroll the content of the page.
When a PaintLayerScrollableArea becomes scrollable - that is, it has overflow: auto or scroll and actuallly has overflowing content - it adds itself to the RootScrollerController's implicit candidate list.
At root scroller evaluation time, we look at each candidate. Those that are no longer valid candidates (i.e. no longer scroll overflow) are removed from the list.
From the remaining entries, we look only at candidates that meet some additional criteria, specified by IsValidImplicit().
If more than one candidate is a valid implicit, we promote the one with the higher z-index. If there‘s a tie, we don’t promote any elements implicitly.
The ViewportScrollCallback is a Scroll Customization apply-scroll callback that's used to affect viewport actions. This object packages together all the things that make scrolling the viewport different from normal elements. Namely, it applies top control movement, scrolling, followed by overscroll effects.
The global rootScroller on a page is responsible for setting the ViewportScrollCallback on the appropriate root scrolling
Element on a page.