The renderer/core/page/scrolling
directory contains utilities and classes for
scrolling that don’t belong anywhere else. For example, the majority of
root scroller implementation as well as overscroll and some scroll customization
types live here.
There are two notions of rootScroller: the effective rootScroller, and the global rootScroller.
The effective rootScroller is the Node
that’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
. The default is to use the
document Node
(hence, why the effective rootScroller is a Node
rather
than an Element
). When the ImplicitRootScrollerEnabled setting is enabled,
Blink may choose an element on the page to make the effective root scroller.
See Implicit Promotion.
The global rootScroller is the one Node
on 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 default effective
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, 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 effective 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
x 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.