Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at */
/* rendering object to wrap rendering objects that should be scrollable */
#include "mozilla/ScrollContainerFrame.h"
#include "ScrollPositionUpdate.h"
#include "mozilla/layers/LayersTypes.h"
#include "nsIXULRuntime.h"
#include "base/compiler_specific.h"
#include "DisplayItemClip.h"
#include "nsCOMPtr.h"
#include "nsIDocumentViewer.h"
#include "nsPresContext.h"
#include "nsView.h"
#include "nsViewportInfo.h"
#include "nsContainerFrame.h"
#include "nsGkAtoms.h"
#include "nsNameSpaceManager.h"
#include "mozilla/intl/BidiEmbeddingLevel.h"
#include "mozilla/dom/DocumentInlines.h"
#include "mozilla/gfx/gfxVars.h"
#include "nsFontMetrics.h"
#include "mozilla/dom/NodeInfo.h"
#include "nsScrollbarFrame.h"
#include "nsINode.h"
#include "nsIScrollbarMediator.h"
#include "nsILayoutHistoryState.h"
#include "nsNodeInfoManager.h"
#include "nsContentCreatorFunctions.h"
#include "nsStyleTransformMatrix.h"
#include "mozilla/PresState.h"
#include "nsContentUtils.h"
#include "nsDisplayList.h"
#include "nsHTMLDocument.h"
#include "nsLayoutUtils.h"
#include "nsBidiPresUtils.h"
#include "nsBidiUtils.h"
#include "nsDocShell.h"
#include "mozilla/ContentEvents.h"
#include "mozilla/DisplayPortUtils.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/Preferences.h"
#include "mozilla/PresShell.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/ScrollbarPreferences.h"
#include "mozilla/ScrollingMetrics.h"
#include "mozilla/StaticPrefs_bidi.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_toolkit.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/SVGOuterSVGFrame.h"
#include "mozilla/ViewportUtils.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/HTMLMarqueeElement.h"
#include "mozilla/dom/ScrollTimeline.h"
#include "mozilla/dom/BrowserChild.h"
#include <stdint.h>
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Telemetry.h"
#include "nsSubDocumentFrame.h"
#include "mozilla/Attributes.h"
#include "ScrollbarActivity.h"
#include "nsRefreshDriver.h"
#include "nsStyleConsts.h"
#include "nsIScrollPositionListener.h"
#include "StickyScrollContainer.h"
#include "nsIFrameInlines.h"
#include "gfxPlatform.h"
#include "mozilla/StaticPrefs_apz.h"
#include "mozilla/StaticPrefs_general.h"
#include "mozilla/StaticPrefs_layers.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/StaticPrefs_mousewheel.h"
#include "mozilla/ToString.h"
#include "ScrollAnimationPhysics.h"
#include "ScrollAnimationBezierPhysics.h"
#include "ScrollAnimationMSDPhysics.h"
#include "ScrollSnap.h"
#include "UnitTransforms.h"
#include "nsSliderFrame.h"
#include "ViewportFrame.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/layers/APZCCallbackHelper.h"
#include "mozilla/layers/APZPublicUtils.h"
#include "mozilla/layers/AxisPhysicsModel.h"
#include "mozilla/layers/AxisPhysicsMSDModel.h"
#include "mozilla/layers/ScrollingInteractionContext.h"
#include "mozilla/layers/ScrollLinkedEffectDetector.h"
#include "mozilla/Unused.h"
#include "MobileViewportManager.h"
#include "TextOverflow.h"
#include "VisualViewport.h"
#include "WindowRenderer.h"
#include <algorithm>
#include <cstdlib> // for std::abs(int/long)
#include <cmath> // for std::abs(float/double)
#include <tuple> // for std::tie
static mozilla::LazyLogModule sApzPaintSkipLog("apz.paintskip");
#define PAINT_SKIP_LOG(...) \
MOZ_LOG(sApzPaintSkipLog, LogLevel::Debug, (__VA_ARGS__))
static mozilla::LazyLogModule sScrollRestoreLog("scrollrestore");
#define SCROLLRESTORE_LOG(...) \
MOZ_LOG(sScrollRestoreLog, LogLevel::Debug, (__VA_ARGS__))
static mozilla::LazyLogModule sRootScrollbarsLog("rootscrollbars");
#define ROOT_SCROLLBAR_LOG(...) \
if (mIsRoot) { \
MOZ_LOG(sRootScrollbarsLog, LogLevel::Debug, (__VA_ARGS__)); \
static mozilla::LazyLogModule sDisplayportLog("apz.displayport");
using namespace mozilla;
using namespace mozilla::dom;
using namespace mozilla::gfx;
using namespace mozilla::layers;
using namespace mozilla::layout;
using nsStyleTransformMatrix::TransformReferenceBox;
static ScrollDirections GetOverflowChange(const nsRect& aCurScrolledRect,
const nsRect& aPrevScrolledRect) {
ScrollDirections result;
if (aPrevScrolledRect.x != aCurScrolledRect.x ||
aPrevScrolledRect.width != aCurScrolledRect.width) {
result += ScrollDirection::eHorizontal;
if (aPrevScrolledRect.y != aCurScrolledRect.y ||
aPrevScrolledRect.height != aCurScrolledRect.height) {
result += ScrollDirection::eVertical;
return result;
* This class handles the dispatching of scroll events to content.
* Scroll events are posted to the refresh driver via
* nsRefreshDriver::PostScrollEvent(), and they are fired during a refresh
* driver tick, after running requestAnimationFrame callbacks but before
* the style flush. This allows rAF callbacks to perform scrolling and have
* that scrolling be reflected on the same refresh driver tick, while at
* the same time allowing scroll event listeners to make style changes and
* have those style changes be reflected on the same refresh driver tick.
* ScrollEvents cannot be refresh observers, because none of the existing
* categories of refresh observers (FlushType::Style, FlushType::Layout,
* and FlushType::Display) are run at the desired time in a refresh driver
* tick. They behave similarly to refresh observers in that their presence
* causes the refresh driver to tick.
* ScrollEvents are one-shot runnables; the refresh driver drops them after
* running them.
class ScrollContainerFrame::ScrollEvent : public Runnable {
explicit ScrollEvent(ScrollContainerFrame* aHelper, bool aDelayed);
void Revoke() { mHelper = nullptr; }
ScrollContainerFrame* mHelper;
class ScrollContainerFrame::ScrollEndEvent : public Runnable {
explicit ScrollEndEvent(ScrollContainerFrame* aHelper, bool aDelayed);
void Revoke() { mHelper = nullptr; }
ScrollContainerFrame* mHelper;
class ScrollContainerFrame::AsyncScrollPortEvent : public Runnable {
explicit AsyncScrollPortEvent(ScrollContainerFrame* helper)
: Runnable("ScrollContainerFrame::AsyncScrollPortEvent"),
mHelper(helper) {}
void Revoke() { mHelper = nullptr; }
ScrollContainerFrame* mHelper;
class ScrollContainerFrame::ScrolledAreaEvent : public Runnable {
explicit ScrolledAreaEvent(ScrollContainerFrame* helper)
: Runnable("ScrollContainerFrame::ScrolledAreaEvent"), mHelper(helper) {}
void Revoke() { mHelper = nullptr; }
ScrollContainerFrame* mHelper;
class ScrollFrameActivityTracker final
: public nsExpirationTracker<ScrollContainerFrame, 4> {
// Wait for 3-4s between scrolls before we remove our layers.
// That's 4 generations of 1s each.
enum { TIMEOUT_MS = 1000 };
explicit ScrollFrameActivityTracker(nsIEventTarget* aEventTarget)
: nsExpirationTracker<ScrollContainerFrame, 4>(
TIMEOUT_MS, "ScrollFrameActivityTracker", aEventTarget) {}
~ScrollFrameActivityTracker() { AgeAllGenerations(); }
virtual void NotifyExpired(ScrollContainerFrame* aObject) override {
static StaticAutoPtr<ScrollFrameActivityTracker> gScrollFrameActivityTracker;
ScrollContainerFrame* NS_NewScrollContainerFrame(mozilla::PresShell* aPresShell,
ComputedStyle* aStyle,
bool aIsRoot) {
return new (aPresShell)
ScrollContainerFrame(aStyle, aPresShell->GetPresContext(), aIsRoot);
ScrollContainerFrame::ScrollContainerFrame(ComputedStyle* aStyle,
nsPresContext* aPresContext,
nsIFrame::ClassID aID, bool aIsRoot)
: nsContainerFrame(aStyle, aPresContext, aID),
mDestination(0, 0),
mRestorePos(-1, -1),
mLastPos(-1, -1),
mApzScrollPos(0, 0),
mLastUpdateFramesPos(-1, -1),
mVelocityQueue(PresContext()) {
if (UsesOverlayScrollbars()) {
mScrollbarActivity = new ScrollbarActivity(this);
if (mIsRoot) {
mZoomableByAPZ = PresShell()->GetZoomableByAPZ();
ScrollContainerFrame::~ScrollContainerFrame() = default;
void ScrollContainerFrame::ScrollbarActivityStarted() const {
if (mScrollbarActivity) {
void ScrollContainerFrame::ScrollbarActivityStopped() const {
if (mScrollbarActivity) {
void ScrollContainerFrame::Destroy(DestroyContext& aContext) {
if (mIsRoot) {
if (mScrollbarActivity) {
mScrollbarActivity = nullptr;
// Unbind the content created in CreateAnonymousContent later...
if (mPostedReflowCallback) {
mPostedReflowCallback = false;
if (mDisplayPortExpiryTimer) {
mDisplayPortExpiryTimer = nullptr;
if (mActivityExpirationState.IsTracked()) {
if (gScrollFrameActivityTracker && gScrollFrameActivityTracker->IsEmpty()) {
gScrollFrameActivityTracker = nullptr;
if (mScrollActivityTimer) {
mScrollActivityTimer = nullptr;
if (mScrollEvent) {
if (mScrollEndEvent) {
void ScrollContainerFrame::SetInitialChildList(ChildListID aListID,
nsFrameList&& aChildList) {
nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList));
void ScrollContainerFrame::AppendFrames(ChildListID aListID,
nsFrameList&& aFrameList) {
NS_ASSERTION(aListID == FrameChildListID::Principal,
"Only main list supported");
mFrames.AppendFrames(nullptr, std::move(aFrameList));
void ScrollContainerFrame::InsertFrames(
ChildListID aListID, nsIFrame* aPrevFrame,
const nsLineList::iterator* aPrevFrameLine, nsFrameList&& aFrameList) {
NS_ASSERTION(aListID == FrameChildListID::Principal,
"Only main list supported");
NS_ASSERTION(!aPrevFrame || aPrevFrame->GetParent() == this,
"inserting after sibling frame with different parent");
mFrames.InsertFrames(nullptr, aPrevFrame, std::move(aFrameList));
void ScrollContainerFrame::RemoveFrame(DestroyContext& aContext,
ChildListID aListID,
nsIFrame* aOldFrame) {
NS_ASSERTION(aListID == FrameChildListID::Principal,
"Only main list supported");
mFrames.DestroyFrame(aContext, aOldFrame);
HTML scrolling implementation
All other things being equal, we prefer layouts with fewer scrollbars showing.
namespace mozilla {
enum class ShowScrollbar : uint8_t {
// Never is a misnomer. We can still get a scrollbar if we need to scroll the
// visual viewport inside the layout viewport. Thus this enum is best thought
// of as value used by layout, which does not know about the visual viewport.
// The visual viewport does not affect any layout sizes, so this is sound.
static ShowScrollbar ShouldShowScrollbar(StyleOverflow aOverflow) {
switch (aOverflow) {
case StyleOverflow::Scroll:
return ShowScrollbar::Always;
case StyleOverflow::Hidden:
return ShowScrollbar::Never;
case StyleOverflow::Auto:
return ShowScrollbar::Auto;
struct MOZ_STACK_CLASS ScrollReflowInput {
// === Filled in by the constructor. Members in this section shouldn't change
// their values after the constructor. ===
const ReflowInput& mReflowInput;
ShowScrollbar mHScrollbar;
// If the horizontal scrollbar is allowed (even if mHScrollbar ==
// ShowScrollbar::Never) provided that it is for scrolling the visual viewport
// inside the layout viewport only.
bool mHScrollbarAllowedForScrollingVVInsideLV = true;
ShowScrollbar mVScrollbar;
// If the vertical scrollbar is allowed (even if mVScrollbar ==
// ShowScrollbar::Never) provided that it is for scrolling the visual viewport
// inside the layout viewport only.
bool mVScrollbarAllowedForScrollingVVInsideLV = true;
nsMargin mComputedBorder;
// === Filled in by ReflowScrolledFrame ===
OverflowAreas mContentsOverflowAreas;
// The scrollbar gutter sizes used in the most recent reflow of
// mScrolledFrame. The writing-mode is the same as the scroll
// container.
LogicalMargin mScrollbarGutterFromLastReflow;
// True if the most recent reflow of mScrolledFrame is with the
// horizontal scrollbar.
bool mReflowedContentsWithHScrollbar = false;
// True if the most recent reflow of mScrolledFrame is with the
// vertical scrollbar.
bool mReflowedContentsWithVScrollbar = false;
// === Filled in when TryLayout succeeds ===
// The size of the inside-border area
nsSize mInsideBorderSize;
// Whether we decided to show the horizontal scrollbar in the most recent
// TryLayout.
bool mShowHScrollbar = false;
// Whether we decided to show the vertical scrollbar in the most recent
// TryLayout.
bool mShowVScrollbar = false;
// If mShow(H|V)Scrollbar is true then
// mOnlyNeed(V|H)ScrollbarToScrollVVInsideLV indicates if the only reason we
// need that scrollbar is to scroll the visual viewport inside the layout
// viewport. These scrollbars are special in that even if they are layout
// scrollbars they do not take up any layout space.
bool mOnlyNeedHScrollbarToScrollVVInsideLV = false;
bool mOnlyNeedVScrollbarToScrollVVInsideLV = false;
ScrollReflowInput(ScrollContainerFrame* aFrame,
const ReflowInput& aReflowInput);
nscoord VScrollbarMinHeight() const { return mVScrollbarPrefSize.height; }
nscoord VScrollbarPrefWidth() const { return mVScrollbarPrefSize.width; }
nscoord HScrollbarMinWidth() const { return mHScrollbarPrefSize.width; }
nscoord HScrollbarPrefHeight() const { return mHScrollbarPrefSize.height; }
// Returns the sizes occupied by the scrollbar gutters. If aShowVScroll or
// aShowHScroll is true, the sizes occupied by the scrollbars are also
// included.
nsMargin ScrollbarGutter(bool aShowVScrollbar, bool aShowHScrollbar,
bool aScrollbarOnRight) const {
if (mOverlayScrollbars) {
return mScrollbarGutter;
nsMargin gutter = mScrollbarGutter;
if (aShowVScrollbar && gutter.right == 0 && gutter.left == 0) {
const nscoord w = VScrollbarPrefWidth();
if (aScrollbarOnRight) {
gutter.right = w;
} else {
gutter.left = w;
if (aShowHScrollbar && gutter.bottom == 0) {
// The horizontal scrollbar is always at the bottom side.
gutter.bottom = HScrollbarPrefHeight();
return gutter;
bool OverlayScrollbars() const { return mOverlayScrollbars; }
// Filled in by the constructor. Put variables here to keep them unchanged
// after initializing them in the constructor.
nsSize mVScrollbarPrefSize;
nsSize mHScrollbarPrefSize;
bool mOverlayScrollbars = false;
// The scrollbar gutter sizes resolved from the scrollbar-gutter and
// scrollbar-width property.
nsMargin mScrollbarGutter;
ScrollReflowInput::ScrollReflowInput(ScrollContainerFrame* aFrame,
const ReflowInput& aReflowInput)
: mReflowInput(aReflowInput),
mComputedBorder(aReflowInput.ComputedPhysicalBorderPadding() -
mScrollbarGutterFromLastReflow(aFrame->GetWritingMode()) {
ScrollStyles styles = aFrame->GetScrollStyles();
mHScrollbar = ShouldShowScrollbar(styles.mHorizontal);
mVScrollbar = ShouldShowScrollbar(styles.mVertical);
mOverlayScrollbars = aFrame->UsesOverlayScrollbars();
if (nsScrollbarFrame* scrollbar = aFrame->GetScrollbarBox(false)) {
mHScrollbarPrefSize = scrollbar->ScrollbarMinSize();
// A zero minimum size is a bug with non-overlay scrollbars. That means
// we'll always try to place the scrollbar, even if it will ultimately not
// fit, see bug 1809630. XUL collapsing is the exception because the
// front-end uses it.
MOZ_ASSERT(mHScrollbarPrefSize.width && mHScrollbarPrefSize.height,
"Shouldn't have a zero horizontal scrollbar-size");
} else {
mHScrollbar = ShowScrollbar::Never;
mHScrollbarAllowedForScrollingVVInsideLV = false;
if (nsScrollbarFrame* scrollbar = aFrame->GetScrollbarBox(true)) {
mVScrollbarPrefSize = scrollbar->ScrollbarMinSize();
// See above.
MOZ_ASSERT(mVScrollbarPrefSize.width && mVScrollbarPrefSize.height,
"Shouldn't have a zero vertical scrollbar-size");
} else {
mVScrollbar = ShowScrollbar::Never;
mVScrollbarAllowedForScrollingVVInsideLV = false;
const auto* scrollbarStyle =
// Hide the scrollbar when the scrollbar-width is set to none.
// Note: In some cases this is unnecessary, because scrollbar-width:none
// makes us suppress scrollbars in CreateAnonymousContent. But if this frame
// initially had a non-'none' scrollbar-width and dynamically changed to
// 'none', then we'll need to handle it here.
const auto scrollbarWidth = scrollbarStyle->StyleUIReset()->ScrollbarWidth();
if (scrollbarWidth == StyleScrollbarWidth::None) {
mHScrollbar = ShowScrollbar::Never;
mHScrollbarAllowedForScrollingVVInsideLV = false;
mVScrollbar = ShowScrollbar::Never;
mVScrollbarAllowedForScrollingVVInsideLV = false;
mScrollbarGutter = aFrame->ComputeStableScrollbarGutter(
scrollbarWidth, scrollbarStyle->StyleDisplay()->mScrollbarGutter);
} // namespace mozilla
static nsSize ComputeInsideBorderSize(const ScrollReflowInput& aState,
const nsSize& aDesiredInsideBorderSize) {
// aDesiredInsideBorderSize is the frame size; i.e., it includes
// borders and padding (but the scrolled child doesn't have
// borders). The scrolled child has the same padding as us.
const WritingMode wm = aState.mReflowInput.GetWritingMode();
const LogicalSize desiredInsideBorderSize(wm, aDesiredInsideBorderSize);
LogicalSize contentSize = aState.mReflowInput.ComputedSize();
const LogicalMargin padding = aState.mReflowInput.ComputedLogicalPadding(wm);
if (contentSize.ISize(wm) == NS_UNCONSTRAINEDSIZE) {
contentSize.ISize(wm) =
desiredInsideBorderSize.ISize(wm) - padding.IStartEnd(wm);
if (contentSize.BSize(wm) == NS_UNCONSTRAINEDSIZE) {
contentSize.BSize(wm) =
desiredInsideBorderSize.BSize(wm) - padding.BStartEnd(wm);
contentSize.ISize(wm) =
contentSize.BSize(wm) =
return (contentSize + padding.Size(wm)).GetPhysicalSize(wm);
* Assuming that we know the metrics for our wrapped frame and
* whether the horizontal and/or vertical scrollbars are present,
* compute the resulting layout and return true if the layout is
* consistent. If the layout is consistent then we fill in the
* computed fields of the ScrollReflowInput.
* The layout is consistent when both scrollbars are showing if and only
* if they should be showing. A horizontal scrollbar should be showing if all
* following conditions are met:
* 1) the style is not HIDDEN
* 2) our inside-border height is at least the scrollbar height (i.e., the
* scrollbar fits vertically)
* 3) the style is SCROLL, or the kid's overflow-area XMost is
* greater than the scrollport width
* @param aForce if true, then we just assume the layout is consistent.
bool ScrollContainerFrame::TryLayout(ScrollReflowInput& aState,
ReflowOutput* aKidMetrics,
bool aAssumeHScroll, bool aAssumeVScroll,
bool aForce) {
if ((aState.mVScrollbar == ShowScrollbar::Never && aAssumeVScroll) ||
(aState.mHScrollbar == ShowScrollbar::Never && aAssumeHScroll)) {
NS_ASSERTION(!aForce, "Shouldn't be forcing a hidden scrollbar to show!");
return false;
const auto wm = GetWritingMode();
const nsMargin scrollbarGutter = aState.ScrollbarGutter(
aAssumeVScroll, aAssumeHScroll, IsScrollbarOnRight());
const LogicalMargin logicalScrollbarGutter(wm, scrollbarGutter);
const bool inlineEndsGutterChanged =
aState.mScrollbarGutterFromLastReflow.IStartEnd(wm) !=
const bool blockEndsGutterChanged =
aState.mScrollbarGutterFromLastReflow.BStartEnd(wm) !=
const bool shouldReflowScrolledFrame =
inlineEndsGutterChanged ||
(blockEndsGutterChanged && ScrolledContentDependsOnBSize(aState));
if (shouldReflowScrolledFrame) {
if (blockEndsGutterChanged) {
"TryLayout reflowing scrolled frame with scrollbars h=%d, v=%d\n",
aAssumeHScroll, aAssumeVScroll);
ReflowScrolledFrame(aState, aAssumeHScroll, aAssumeVScroll, aKidMetrics);
const nsSize scrollbarGutterSize(scrollbarGutter.LeftRight(),
// First, compute our inside-border size and scrollport size
nsSize kidSize = GetContainSizeAxes().ContainSize(
aKidMetrics->PhysicalSize(), *aState.mReflowInput.mFrame);
const nsSize desiredInsideBorderSize = kidSize + scrollbarGutterSize;
aState.mInsideBorderSize =
ComputeInsideBorderSize(aState, desiredInsideBorderSize);
nsSize layoutSize =
mIsUsingMinimumScaleSize ? mMinimumScaleSize : aState.mInsideBorderSize;
const nsSize scrollPortSize =
Max(nsSize(0, 0), layoutSize - scrollbarGutterSize);
if (mIsUsingMinimumScaleSize) {
mICBSize =
Max(nsSize(0, 0), aState.mInsideBorderSize - scrollbarGutterSize);
nsSize visualViewportSize = scrollPortSize;
ROOT_SCROLLBAR_LOG("TryLayout with VV %s\n",
mozilla::PresShell* presShell = PresShell();
// Note: we check for a non-null MobileViepwortManager here, but ideally we
// should be able to drop that clause as well. It's just that in some cases
// with extension popups the composition size comes back as stale, because
// the content viewer is only resized after the popup contents are reflowed.
// That case also happens to have no APZ and no MVM, so we use that as a
// way to detect the scenario. Bug 1648669 tracks removing this clause.
if (mIsRoot && presShell->GetMobileViewportManager()) {
visualViewportSize = nsLayoutUtils::CalculateCompositionSizeForFrame(
this, false, &layoutSize);
visualViewportSize =
Max(nsSize(0, 0), visualViewportSize - scrollbarGutterSize);
float resolution = presShell->GetResolution();
visualViewportSize.width /= resolution;
visualViewportSize.height /= resolution;
ROOT_SCROLLBAR_LOG("TryLayout now with VV %s\n",
nsRect overflowRect = aState.mContentsOverflowAreas.ScrollableOverflow();
// If the content height expanded by the minimum-scale will be taller than
// the scrollable overflow area, we need to expand the area here to tell
// properly whether we need to render the overlay vertical scrollbar.
// NOTE: This expanded size should NOT be used for non-overley scrollbars
// cases since putting the vertical non-overlay scrollbar will make the
// content width narrow a little bit, which in turn the minimum scale value
// becomes a bit bigger than before, then the vertical scrollbar is no longer
// needed, which means the content width becomes the original width, then the
// minimum-scale is changed to the original one, and so forth.
if (UsesOverlayScrollbars() && mIsUsingMinimumScaleSize &&
mMinimumScaleSize.height > overflowRect.YMost()) {
overflowRect.height += mMinimumScaleSize.height - overflowRect.YMost();
nsRect scrolledRect =
GetUnsnappedScrolledRectInternal(overflowRect, scrollPortSize);
"TryLayout scrolledRect:%s overflowRect:%s scrollportSize:%s\n",
ToString(scrolledRect).c_str(), ToString(overflowRect).c_str(),
nscoord oneDevPixel = PresContext()->DevPixelsToAppUnits(1);
bool showHScrollbar = aAssumeHScroll;
bool showVScrollbar = aAssumeVScroll;
if (!aForce) {
nsSize sizeToCompare = visualViewportSize;
if (gfxPlatform::UseDesktopZoomingScrollbars()) {
sizeToCompare = scrollPortSize;
// No need to compute showHScrollbar if we got ShowScrollbar::Never.
if (aState.mHScrollbar != ShowScrollbar::Never) {
showHScrollbar =
aState.mHScrollbar == ShowScrollbar::Always ||
scrolledRect.XMost() >= sizeToCompare.width + oneDevPixel ||
scrolledRect.x <= -oneDevPixel;
// TODO(emilio): This should probably check this scrollbar's minimum size
// in both axes, for consistency?
if (aState.mHScrollbar == ShowScrollbar::Auto &&
scrollPortSize.width < aState.HScrollbarMinWidth()) {
showHScrollbar = false;
ROOT_SCROLLBAR_LOG("TryLayout wants H Scrollbar: %d =? %d\n",
showHScrollbar, aAssumeHScroll);
// No need to compute showVScrollbar if we got ShowScrollbar::Never.
if (aState.mVScrollbar != ShowScrollbar::Never) {
showVScrollbar =
aState.mVScrollbar == ShowScrollbar::Always ||
scrolledRect.YMost() >= sizeToCompare.height + oneDevPixel ||
scrolledRect.y <= -oneDevPixel;
// TODO(emilio): This should probably check this scrollbar's minimum size
// in both axes, for consistency?
if (aState.mVScrollbar == ShowScrollbar::Auto &&
scrollPortSize.height < aState.VScrollbarMinHeight()) {
showVScrollbar = false;
ROOT_SCROLLBAR_LOG("TryLayout wants V Scrollbar: %d =? %d\n",
showVScrollbar, aAssumeVScroll);
if (showHScrollbar != aAssumeHScroll || showVScrollbar != aAssumeVScroll) {
const nsMargin wantedScrollbarGutter = aState.ScrollbarGutter(
showVScrollbar, showHScrollbar, IsScrollbarOnRight());
// We report an inconsistent layout only when the desired visibility of
// the scrollbars can change the size of the scrollbar gutters.
if (scrollbarGutter != wantedScrollbarGutter) {
return false;
// If we reach here, the layout is consistent. Record the desired visibility
// of the scrollbars.
aState.mShowHScrollbar = showHScrollbar;
aState.mShowVScrollbar = showVScrollbar;
const nsPoint scrollPortOrigin(
aState.mComputedBorder.left + scrollbarGutter.left, +;
SetScrollPort(nsRect(scrollPortOrigin, scrollPortSize));
if (mIsRoot && gfxPlatform::UseDesktopZoomingScrollbars()) {
bool vvChanged = true;
const bool overlay = aState.OverlayScrollbars();
// This loop can run at most twice since we can only add a scrollbar once.
// At this point we've already decided that this layout is consistent so we
// will return true. Scrollbars added here never take up layout space even
// if they are layout scrollbars so any changes made here will not make us
// return false.
while (vvChanged) {
vvChanged = false;
if (!aState.mShowHScrollbar &&
aState.mHScrollbarAllowedForScrollingVVInsideLV) {
if (ScrollPort().width >= visualViewportSize.width + oneDevPixel &&
(overlay ||
visualViewportSize.width >= aState.HScrollbarMinWidth())) {
vvChanged = true;
if (!overlay) {
visualViewportSize.height -= aState.HScrollbarPrefHeight();
aState.mShowHScrollbar = true;
aState.mOnlyNeedHScrollbarToScrollVVInsideLV = true;
ROOT_SCROLLBAR_LOG("TryLayout added H scrollbar for VV, VV now %s\n",
if (!aState.mShowVScrollbar &&
aState.mVScrollbarAllowedForScrollingVVInsideLV) {
if (ScrollPort().height >= visualViewportSize.height + oneDevPixel &&
(overlay ||
visualViewportSize.height >= aState.VScrollbarMinHeight())) {
vvChanged = true;
if (!overlay) {
visualViewportSize.width -= aState.VScrollbarPrefWidth();
aState.mShowVScrollbar = true;
aState.mOnlyNeedVScrollbarToScrollVVInsideLV = true;
ROOT_SCROLLBAR_LOG("TryLayout added V scrollbar for VV, VV now %s\n",
return true;
bool ScrollContainerFrame::ScrolledContentDependsOnBSize(
const ScrollReflowInput& aState) const {
return mScrolledFrame->HasAnyStateBits(
aState.mReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE ||
aState.mReflowInput.ComputedMinBSize() > 0 ||
aState.mReflowInput.ComputedMaxBSize() != NS_UNCONSTRAINEDSIZE;
void ScrollContainerFrame::ReflowScrolledFrame(ScrollReflowInput& aState,
bool aAssumeHScroll,
bool aAssumeVScroll,
ReflowOutput* aMetrics) {
const WritingMode wm = GetWritingMode();
// these could be NS_UNCONSTRAINEDSIZE ... std::min arithmetic should
// be OK
LogicalMargin padding = aState.mReflowInput.ComputedLogicalPadding(wm);
nscoord availISize =
aState.mReflowInput.ComputedISize() + padding.IStartEnd(wm);
nscoord computedBSize = aState.mReflowInput.ComputedBSize();
nscoord computedMinBSize = aState.mReflowInput.ComputedMinBSize();
nscoord computedMaxBSize = aState.mReflowInput.ComputedMaxBSize();
if (!ShouldPropagateComputedBSizeToScrolledContent()) {
computedMinBSize = 0;
const LogicalMargin scrollbarGutter(
wm, aState.ScrollbarGutter(aAssumeVScroll, aAssumeHScroll,
if (const nscoord inlineEndsGutter = scrollbarGutter.IStartEnd(wm);
inlineEndsGutter > 0) {
availISize = std::max(0, availISize - inlineEndsGutter);
if (const nscoord blockEndsGutter = scrollbarGutter.BStartEnd(wm);
blockEndsGutter > 0) {
if (computedBSize != NS_UNCONSTRAINEDSIZE) {
computedBSize = std::max(0, computedBSize - blockEndsGutter);
computedMinBSize = std::max(0, computedMinBSize - blockEndsGutter);
if (computedMaxBSize != NS_UNCONSTRAINEDSIZE) {
computedMaxBSize = std::max(0, computedMaxBSize - blockEndsGutter);
nsPresContext* presContext = PresContext();
// Pass InitFlags::CallerWillInit so we can pass in the correct padding.
ReflowInput kidReflowInput(presContext, aState.mReflowInput, mScrolledFrame,
LogicalSize(wm, availISize, NS_UNCONSTRAINEDSIZE),
Nothing(), ReflowInput::InitFlag::CallerWillInit);
const WritingMode kidWM = kidReflowInput.GetWritingMode();
kidReflowInput.Init(presContext, Nothing(), Nothing(),
Some(padding.ConvertTo(kidWM, wm)));
kidReflowInput.mFlags.mAssumingHScrollbar = aAssumeHScroll;
kidReflowInput.mFlags.mAssumingVScrollbar = aAssumeVScroll;
kidReflowInput.mFlags.mTreatBSizeAsIndefinite =
if (aState.mReflowInput.IsBResizeForWM(kidWM)) {
if (aState.mReflowInput.IsBResizeForPercentagesForWM(kidWM)) {
kidReflowInput.mFlags.mIsBResizeForPercentages = true;
// Temporarily set mHasHorizontalScrollbar/mHasVerticalScrollbar to
// reflect our assumptions while we reflow the child.
bool didHaveHorizontalScrollbar = mHasHorizontalScrollbar;
bool didHaveVerticalScrollbar = mHasVerticalScrollbar;
mHasHorizontalScrollbar = aAssumeHScroll;
mHasVerticalScrollbar = aAssumeVScroll;
nsReflowStatus status;
// No need to pass a true container-size to ReflowChild or
// FinishReflowChild, because it's only used there when positioning
// the frame (i.e. if ReflowChildFlags::NoMoveFrame isn't set)
const nsSize dummyContainerSize;
ReflowChild(mScrolledFrame, presContext, *aMetrics, kidReflowInput, wm,
LogicalPoint(wm), dummyContainerSize,
ReflowChildFlags::NoMoveFrame, status);
mHasHorizontalScrollbar = didHaveHorizontalScrollbar;
mHasVerticalScrollbar = didHaveVerticalScrollbar;
// Don't resize or position the view (if any) because we're going to resize
// it to the correct size anyway in PlaceScrollArea. Allowing it to
// resize here would size it to the natural height of the frame,
// which will usually be different from the scrollport height;
// invalidating the difference will cause unnecessary repainting.
mScrolledFrame, presContext, *aMetrics, &kidReflowInput, wm,
LogicalPoint(wm), dummyContainerSize,
ReflowChildFlags::NoMoveFrame | ReflowChildFlags::NoSizeView);
if (mScrolledFrame->HasAnyStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE)) {
// Propagate NS_FRAME_CONTAINS_RELATIVE_BSIZE from our inner scrolled frame
// to ourselves so that our containing block is aware of it.
// Note: If the scrolled frame has any child whose block-size depends on the
// containing block's block-size, the NS_FRAME_CONTAINS_RELATIVE_BSIZE bit
// is set on the scrolled frame when initializing the child's ReflowInput in
// ReflowInput::InitResizeFlags(). Therefore, we propagate the bit here
// after we reflowed the scrolled frame.
// XXX Some frames (e.g. nsFrameFrame, nsTextFrame) don't
// bother setting their mOverflowArea. This is wrong because every frame
// should always set mOverflowArea. In fact nsFrameFrame doesn't
// support the 'outline' property because of this. Rather than fix the
// world right now, just fix up the overflow area if necessary. Note that we
// don't check HasOverflowRect() because it could be set even though the
// overflow area doesn't include the frame bounds.
auto* disp = StyleDisplay();
if (MOZ_UNLIKELY(disp->mOverflowClipBoxInline ==
StyleOverflowClipBox::ContentBox)) {
// The scrolled frame is scrollable in the inline axis with
// `overflow-clip-box:content-box`. To prevent its content from being
// clipped at the scroll container's padding edges, we inflate its
// children's scrollable overflow area with its inline padding, and union
// its scrollable overflow area with its children's inflated scrollable
// overflow area.
OverflowAreas childOverflow;
nsRect childScrollableOverflow = childOverflow.ScrollableOverflow();
const LogicalMargin inlinePadding =
padding.ApplySkipSides(LogicalSides(wm, LogicalSides::BBoth));
nsRect& so = aMetrics->ScrollableOverflow();
so = so.UnionEdges(childScrollableOverflow);
aState.mContentsOverflowAreas = aMetrics->mOverflowAreas;
aState.mScrollbarGutterFromLastReflow = scrollbarGutter;
aState.mReflowedContentsWithHScrollbar = aAssumeHScroll;
aState.mReflowedContentsWithVScrollbar = aAssumeVScroll;
bool ScrollContainerFrame::GuessHScrollbarNeeded(
const ScrollReflowInput& aState) {
if (aState.mHScrollbar != ShowScrollbar::Auto) {
// no guessing required
return aState.mHScrollbar == ShowScrollbar::Always;
// We only care about scrollbars that might take up space when trying to guess
// if we need a scrollbar, so we ignore scrollbars only created to scroll the
// visual viewport inside the layout viewport because they take up no layout
// space.
return mHasHorizontalScrollbar && !mOnlyNeedHScrollbarToScrollVVInsideLV;
bool ScrollContainerFrame::GuessVScrollbarNeeded(
const ScrollReflowInput& aState) {
if (aState.mVScrollbar != ShowScrollbar::Auto) {
// no guessing required
return aState.mVScrollbar == ShowScrollbar::Always;
// If we've had at least one non-initial reflow, then just assume
// the state of the vertical scrollbar will be what we determined
// last time.
if (mHadNonInitialReflow) {
// We only care about scrollbars that might take up space when trying to
// guess if we need a scrollbar, so we ignore scrollbars only created to
// scroll the visual viewport inside the layout viewport because they take
// up no layout space.
return mHasVerticalScrollbar && !mOnlyNeedVScrollbarToScrollVVInsideLV;
// If this is the initial reflow, guess false because usually
// we have very little content by then.
if (InInitialReflow()) return false;
if (mIsRoot) {
nsIFrame* f = mScrolledFrame->PrincipalChildList().FirstChild();
if (f && f->IsSVGOuterSVGFrame() &&
static_cast<SVGOuterSVGFrame*>(f)->VerticalScrollbarNotNeeded()) {
// Common SVG case - avoid a bad guess.
return false;
// Assume that there will be a scrollbar; it seems to me
// that 'most pages' do have a scrollbar, and anyway, it's cheaper
// to do an extra reflow for the pages that *don't* need a
// scrollbar (because on average they will have less content).
return true;
// For non-viewports, just guess that we don't need a scrollbar.
// XXX I wonder if statistically this is the right idea; I'm
// basically guessing that there are a lot of overflow:auto DIVs
// that get their intrinsic size and don't overflow
return false;
bool ScrollContainerFrame::InInitialReflow() const {
// We're in an initial reflow if NS_FRAME_FIRST_REFLOW is set, unless we're a
// root scrollframe. In that case we want to skip this clause altogether.
// The guess here is that there are lots of overflow:auto divs out there that
// end up auto-sizing so they don't overflow, and that the root basically
// always needs a scrollbar if it did last time we loaded this page (good
// assumption, because our initial reflow is no longer synchronous).
return !mIsRoot && HasAnyStateBits(NS_FRAME_FIRST_REFLOW);
void ScrollContainerFrame::ReflowContents(ScrollReflowInput& aState,
const ReflowOutput& aDesiredSize) {
const WritingMode desiredWm = aDesiredSize.GetWritingMode();
ReflowOutput kidDesiredSize(desiredWm);
ReflowScrolledFrame(aState, GuessHScrollbarNeeded(aState),
GuessVScrollbarNeeded(aState), &kidDesiredSize);
// There's an important special case ... if the child appears to fit
// in the inside-border rect (but overflows the scrollport), we
// should try laying it out without a vertical scrollbar. It will
// usually fit because making the available-width wider will not
// normally make the child taller. (The only situation I can think
// of is when you have a line containing %-width inline replaced
// elements whose percentages sum to more than 100%, so increasing
// the available width makes the line break where it was fitting
// before.) If we don't treat this case specially, then we will
// decide that showing scrollbars is OK because the content
// overflows when we're showing scrollbars and we won't try to
// remove the vertical scrollbar.
// Detecting when we enter this special case is important for when
// people design layouts that exactly fit the container "most of the
// time".
// XXX Is this check really sufficient to catch all the incremental cases
// where the ideal case doesn't have a scrollbar?
if ((aState.mReflowedContentsWithHScrollbar ||
aState.mReflowedContentsWithVScrollbar) &&
aState.mVScrollbar != ShowScrollbar::Always &&
aState.mHScrollbar != ShowScrollbar::Always) {
nsSize kidSize = GetContainSizeAxes().ContainSize(
kidDesiredSize.PhysicalSize(), *aState.mReflowInput.mFrame);
nsSize insideBorderSize = ComputeInsideBorderSize(aState, kidSize);
nsRect scrolledRect = GetUnsnappedScrolledRectInternal(
kidDesiredSize.ScrollableOverflow(), insideBorderSize);
if (nsRect(nsPoint(0, 0), insideBorderSize).Contains(scrolledRect)) {
// Let's pretend we had no scrollbars coming in here
ReflowScrolledFrame(aState, false, false, &kidDesiredSize);
if (IsRootScrollFrameOfDocument()) {
// Try vertical scrollbar settings that leave the vertical scrollbar
// unchanged. Do this first because changing the vertical scrollbar setting is
// expensive, forcing a reflow always.
// Try leaving the horizontal scrollbar unchanged first. This will be more
// efficient.
ROOT_SCROLLBAR_LOG("Trying layout1 with %d, %d\n",
if (TryLayout(aState, &kidDesiredSize, aState.mReflowedContentsWithHScrollbar,
aState.mReflowedContentsWithVScrollbar, false)) {
ROOT_SCROLLBAR_LOG("Trying layout2 with %d, %d\n",
if (TryLayout(aState, &kidDesiredSize,
aState.mReflowedContentsWithVScrollbar, false)) {
// OK, now try toggling the vertical scrollbar. The performance advantage
// of trying the status-quo horizontal scrollbar state
// does not exist here (we'll have to reflow due to the vertical scrollbar
// change), so always try no horizontal scrollbar first.
bool newVScrollbarState = !aState.mReflowedContentsWithVScrollbar;
ROOT_SCROLLBAR_LOG("Trying layout3 with %d, %d\n", false, newVScrollbarState);
if (TryLayout(aState, &kidDesiredSize, false, newVScrollbarState, false)) {
ROOT_SCROLLBAR_LOG("Trying layout4 with %d, %d\n", true, newVScrollbarState);
if (TryLayout(aState, &kidDesiredSize, true, newVScrollbarState, false)) {
// OK, we're out of ideas. Try again enabling whatever scrollbars we can
// enable and force the layout to stick even if it's inconsistent.
// This just happens sometimes.
ROOT_SCROLLBAR_LOG("Giving up, adding both scrollbars...\n");
TryLayout(aState, &kidDesiredSize, aState.mHScrollbar != ShowScrollbar::Never,
aState.mVScrollbar != ShowScrollbar::Never, true);
void ScrollContainerFrame::PlaceScrollArea(ScrollReflowInput& aState,
const nsPoint& aScrollPosition) {
// Set the x,y of the scrolled frame to the correct value
mScrolledFrame->SetPosition(ScrollPort().TopLeft() - aScrollPosition);
// Recompute our scrollable overflow, taking perspective children into
// account. Note that this only recomputes the overflow areas stored on the
// helper (which are used to compute scrollable length and scrollbar thumb
// sizes) but not the overflow areas stored on the frame. This seems to work
// for now, but it's possible that we may need to update both in the future.
// Preserve the width or height of empty rects
const nsSize portSize = ScrollPort().Size();
nsRect scrolledRect = GetUnsnappedScrolledRectInternal(
aState.mContentsOverflowAreas.ScrollableOverflow(), portSize);
nsRect scrolledArea =
scrolledRect.UnionEdges(nsRect(nsPoint(0, 0), portSize));
// Store the new overflow area. Note that this changes where an outline
// of the scrolled frame would be painted, but scrolled frames can't have
// outlines (the outline would go on this scrollframe instead).
// Using FinishAndStoreOverflow is needed so the overflow rect gets set
// correctly. It also messes with the overflow rect in the 'clip' case, but
// scrolled frames can't have 'overflow' either.
// This needs to happen before SyncFrameViewAfterReflow so
// HasOverflowRect() will return the correct value.
OverflowAreas overflow(scrolledArea, scrolledArea);
mScrolledFrame->FinishAndStoreOverflow(overflow, mScrolledFrame->GetSize());
// Note that making the view *exactly* the size of the scrolled area
// is critical, since the view scrolling code uses the size of the
// scrolled view to clamp scroll requests.
// Normally the mScrolledFrame won't have a view but in some cases it
// might create its own.
mScrolledFrame->PresContext(), mScrolledFrame, mScrolledFrame->GetView(),
scrolledArea, ReflowChildFlags::Default);
nscoord ScrollContainerFrame::IntrinsicScrollbarGutterSizeAtInlineEdges()
const {
const auto wm = GetWritingMode();
const LogicalMargin gutter(wm, IntrinsicScrollbarGutterSize());
return gutter.IStartEnd(wm);
nsMargin ScrollContainerFrame::IntrinsicScrollbarGutterSize() const {
if (PresContext()->UseOverlayScrollbars()) {
// Overlay scrollbars do not consume space per spec.
return {};
const auto* styleForScrollbar = nsLayoutUtils::StyleForScrollbar(this);
const auto& styleScrollbarWidth =
if (styleScrollbarWidth == StyleScrollbarWidth::None) {
// Scrollbar shouldn't appear at all with "scrollbar-width: none".
return {};
const auto& styleScrollbarGutter =
nsMargin gutter =
ComputeStableScrollbarGutter(styleScrollbarWidth, styleScrollbarGutter);
if (gutter.LeftRight() == 0 || gutter.TopBottom() == 0) {
// If there is no stable scrollbar-gutter at vertical or horizontal
// dimension, check if a scrollbar is always shown at that dimension.
ScrollStyles scrollStyles = GetScrollStyles();
const nscoord scrollbarSize =
GetNonOverlayScrollbarSize(PresContext(), styleScrollbarWidth);
if (gutter.LeftRight() == 0 &&
scrollStyles.mVertical == StyleOverflow::Scroll) {
(IsScrollbarOnRight() ? gutter.right : gutter.left) = scrollbarSize;
if (gutter.TopBottom() == 0 &&
scrollStyles.mHorizontal == StyleOverflow::Scroll) {
// The horizontal scrollbar is always at the bottom side.
gutter.bottom = scrollbarSize;
return gutter;
nsMargin ScrollContainerFrame::ComputeStableScrollbarGutter(
const StyleScrollbarWidth& aStyleScrollbarWidth,
const StyleScrollbarGutter& aStyleScrollbarGutter) const {
if (PresContext()->UseOverlayScrollbars()) {
// Overlay scrollbars do not consume space per spec.
return {};
if (aStyleScrollbarWidth == StyleScrollbarWidth::None) {
// Scrollbar shouldn't appear at all with "scrollbar-width: none".
return {};
if (aStyleScrollbarGutter == StyleScrollbarGutter::AUTO) {
// Scrollbars create space depending on the 'overflow' property and whether
// the content overflows. Callers need to check this scenario if they want
// to consider the space created by the actual scrollbars.
return {};
const bool bothEdges =
bool(aStyleScrollbarGutter & StyleScrollbarGutter::BOTH_EDGES);
const bool isVerticalWM = GetWritingMode().IsVertical();
const nscoord scrollbarSize =
GetNonOverlayScrollbarSize(PresContext(), aStyleScrollbarWidth);
nsMargin scrollbarGutter;
if (bothEdges) {
if (isVerticalWM) { = scrollbarGutter.bottom = scrollbarSize;
} else {
scrollbarGutter.left = scrollbarGutter.right = scrollbarSize;
} else {
MOZ_ASSERT(bool(aStyleScrollbarGutter & StyleScrollbarGutter::STABLE),
"scrollbar-gutter value should be 'stable'!");
if (isVerticalWM) {
// The horizontal scrollbar-gutter is always at the bottom side.
scrollbarGutter.bottom = scrollbarSize;
} else if (IsScrollbarOnRight()) {
scrollbarGutter.right = scrollbarSize;
} else {
scrollbarGutter.left = scrollbarSize;
return scrollbarGutter;
// Legacy, this sucks!
static bool IsMarqueeScrollbox(const nsIFrame& aScrollFrame) {
return HTMLMarqueeElement::FromNodeOrNull(aScrollFrame.GetContent());
/* virtual */
nscoord ScrollContainerFrame::GetMinISize(gfxContext* aRenderingContext) {
nscoord result = [&] {
if (const Maybe<nscoord> containISize = ContainIntrinsicISize()) {
return *containISize;
if (MOZ_UNLIKELY(IsMarqueeScrollbox(*this))) {
return 0;
return mScrolledFrame->GetMinISize(aRenderingContext);
return result + IntrinsicScrollbarGutterSizeAtInlineEdges();
/* virtual */
nscoord ScrollContainerFrame::GetPrefISize(gfxContext* aRenderingContext) {
const Maybe<nscoord> containISize = ContainIntrinsicISize();
nscoord result = containISize
? *containISize
: mScrolledFrame->GetPrefISize(aRenderingContext);
return NSCoordSaturatingAdd(result,
// When we have perspective set on the outer scroll frame, and transformed
// children (possibly with preserve-3d) then the effective transform on the
// child depends on the offset to the scroll frame, which changes as we scroll.
// This perspective transform can cause the element to move relative to the
// scrolled inner frame, which would cause the scrollable length changes during
// scrolling if we didn't account for it. Since we don't want scrollHeight/Width
// and the size of scrollbar thumbs to change during scrolling, we compute the
// scrollable overflow by determining the scroll position at which the child
// becomes completely visible within the scrollport rather than using the union
// of the overflow areas at their current position.
static void GetScrollableOverflowForPerspective(
nsIFrame* aScrolledFrame, nsIFrame* aCurrentFrame, const nsRect aScrollPort,
nsPoint aOffset, nsRect& aScrolledFrameOverflowArea) {
// Iterate over all children except pop-ups.
for (const auto& [list, listID] : aCurrentFrame->ChildLists()) {
for (nsIFrame* child : list) {
nsPoint offset = aOffset;
// When we reach a direct child of the scroll, then we record the offset
// to convert from that frame's coordinate into the scroll frame's
// coordinates. Preserve-3d descendant frames use the same offset as their
// ancestors, since TransformRect already converts us into the coordinate
// space of the preserve-3d root.
if (aScrolledFrame == aCurrentFrame) {
offset = child->GetPosition();
if (child->Extend3DContext()) {
// If we're a preserve-3d frame, then recurse and include our
// descendants since overflow of preserve-3d frames is only included
// in the post-transform overflow area of the preserve-3d root frame.
GetScrollableOverflowForPerspective(aScrolledFrame, child, aScrollPort,
offset, aScrolledFrameOverflowArea);
// If we're transformed, then we want to consider the possibility that
// this frame might move relative to the scrolled frame when scrolling.
// For preserve-3d, leaf frames have correct overflow rects relative to
// themselves. preserve-3d 'nodes' (intermediate frames and the root) have
// only their untransformed children included in their overflow relative
// to self, which is what we want to include here.
if (child->IsTransformed()) {
// Compute the overflow rect for this leaf transform frame in the
// coordinate space of the scrolled frame.
nsPoint scrollPos = aScrolledFrame->GetPosition();
nsRect preScroll, postScroll;
// TODO: Can we reuse the reference box?
TransformReferenceBox refBox(child);
preScroll = nsDisplayTransform::TransformRect(
child->ScrollableOverflowRectRelativeToSelf(), child, refBox);
// Temporarily override the scroll position of the scrolled frame by
// 10 CSS pixels, and then recompute what the overflow rect would be.
// This scroll position may not be valid, but that shouldn't matter
// for our calculations.
aScrolledFrame->SetPosition(scrollPos + nsPoint(600, 600));
TransformReferenceBox refBox(child);
postScroll = nsDisplayTransform::TransformRect(
child->ScrollableOverflowRectRelativeToSelf(), child, refBox);
// Compute how many app units the overflow rects moves by when we adjust
// the scroll position by 1 app unit.
double rightDelta =
(postScroll.XMost() - preScroll.XMost() + 600.0) / 600.0;
double bottomDelta =
(postScroll.YMost() - preScroll.YMost() + 600.0) / 600.0;
// We can't ever have negative scrolling.
NS_ASSERTION(rightDelta > 0.0f && bottomDelta > 0.0f,
"Scrolling can't be reversed!");
// Move preScroll into the coordinate space of the scrollport.
preScroll += offset + scrollPos;
// For each of the four edges of preScroll, figure out how far they
// extend beyond the scrollport. Ignore negative values since that means
// that side is already scrolled in to view and we don't need to add
// overflow to account for it.
nsMargin overhang(std::max(0, aScrollPort.Y() - preScroll.Y()),
std::max(0, preScroll.XMost() - aScrollPort.XMost()),
std::max(0, preScroll.YMost() - aScrollPort.YMost()),
std::max(0, aScrollPort.X() - preScroll.X()));
// Scale according to rightDelta/bottomDelta to adjust for the different
// scroll rates. = NSCoordSaturatingMultiply(, static_cast<float>(1 / bottomDelta));
overhang.right = NSCoordSaturatingMultiply(
overhang.right, static_cast<float>(1 / rightDelta));
overhang.bottom = NSCoordSaturatingMultiply(
overhang.bottom, static_cast<float>(1 / bottomDelta));
overhang.left = NSCoordSaturatingMultiply(
overhang.left, static_cast<float>(1 / rightDelta));
// Take the minimum overflow rect that would allow the current scroll
// position, using the size of the scroll port and offset by the
// inverse of the scroll position.
nsRect overflow = aScrollPort - scrollPos;
// Expand it by our margins to get an overflow rect that would allow all
// edges of our transformed content to be scrolled into view.
// Merge it with the combined overflow
} else if (aCurrentFrame == aScrolledFrame) {
BaselineSharingGroup ScrollContainerFrame::GetDefaultBaselineSharingGroup()
const {
return mScrolledFrame->GetDefaultBaselineSharingGroup();
nscoord ScrollContainerFrame::SynthesizeFallbackBaseline(
mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup) const {
// Marign-end even for central baselines.
if (aWM.IsLineInverted()) {
return -GetLogicalUsedMargin(aWM).BStart(aWM);
return aBaselineGroup == BaselineSharingGroup::First
? BSize(aWM) + GetLogicalUsedMargin(aWM).BEnd(aWM)
: -GetLogicalUsedMargin(aWM).BEnd(aWM);
Maybe<nscoord> ScrollContainerFrame::GetNaturalBaselineBOffset(
WritingMode aWM, BaselineSharingGroup aBaselineGroup,
BaselineExportContext aExportContext) const {
// Block containers that are scrollable always have a last baseline
// that are synthesized from block-end margin edge.
// Note(dshin): This behaviour is really only relevant to `inline-block`
// alignment context. In the context of table/flex/grid alignment, first/last
// baselines are calculated through `GetFirstLineBaseline`, which does
// calculations of its own.
if (aExportContext == BaselineExportContext::LineLayout &&
aBaselineGroup == BaselineSharingGroup::Last &&
mScrolledFrame->IsBlockFrameOrSubclass()) {
return Some(SynthesizeFallbackBaseline(aWM, aBaselineGroup));
if (StyleDisplay()->IsContainLayout()) {
return Nothing{};
// OK, here's where we defer to our scrolled frame.
return mScrolledFrame
->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext)
.map([this, aWM](nscoord aBaseline) {
// We have to add our border BStart thickness to whatever it returns, to
// produce an offset in our frame-rect's coordinate system. (We don't
// have to add padding, because the scrolled frame handles our padding.)
LogicalMargin border = GetLogicalUsedBorder(aWM);
const auto bSize = GetLogicalSize(aWM).BSize(aWM);
// Clamp the baseline to the border rect. See bug 1791069.
return std::clamp(border.BStart(aWM) + aBaseline, 0, bSize);
void ScrollContainerFrame::AdjustForPerspective(nsRect& aScrollableOverflow) {
// If we have perspective that is being applied to our children, then
// the effective transform on the child depends on the relative position
// of the child to us and changes during scrolling.
if (!ChildrenHavePerspective()) {
GetScrollableOverflowForPerspective(mScrolledFrame, mScrolledFrame,
ScrollPort(), nsPoint(),
void ScrollContainerFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
ScrollReflowInput state(this, aReflowInput);
//------------ Handle Incremental Reflow -----------------
bool reflowHScrollbar = true;
bool reflowVScrollbar = true;
bool reflowScrollCorner = true;
if (!aReflowInput.ShouldReflowAllKids()) {
auto NeedsReflow = [](const nsIFrame* aFrame) {
return aFrame && aFrame->IsSubtreeDirty();
reflowHScrollbar = NeedsReflow(mHScrollbarBox);
reflowVScrollbar = NeedsReflow(mVScrollbarBox);
reflowScrollCorner =
NeedsReflow(mScrollCornerBox) || NeedsReflow(mResizerBox);
if (mIsRoot) {
reflowScrollCorner = false;
const nsRect oldScrollPort = ScrollPort();
nsRect oldScrolledAreaBounds =
nsPoint oldScrollPosition = GetScrollPosition();
ReflowContents(state, aDesiredSize);
nsSize layoutSize =
mIsUsingMinimumScaleSize ? mMinimumScaleSize : state.mInsideBorderSize;
aDesiredSize.Width() = layoutSize.width + state.mComputedBorder.LeftRight();
aDesiredSize.Height() = layoutSize.height + state.mComputedBorder.TopBottom();
// Set the size of the frame now since computing the perspective-correct
// overflow (within PlaceScrollArea) can rely on it.
// Restore the old scroll position, for now, even if that's not valid anymore
// because we changed size. We'll fix it up in a post-reflow callback, because
// our current size may only be temporary (e.g. we're compute XUL desired
// sizes).
PlaceScrollArea(state, oldScrollPosition);
if (!mPostedReflowCallback) {
// Make sure we'll try scrolling to restored position
mPostedReflowCallback = true;
bool didOnlyHScrollbar = mOnlyNeedHScrollbarToScrollVVInsideLV;
bool didOnlyVScrollbar = mOnlyNeedVScrollbarToScrollVVInsideLV;
mOnlyNeedHScrollbarToScrollVVInsideLV =
mOnlyNeedVScrollbarToScrollVVInsideLV =
bool didHaveHScrollbar = mHasHorizontalScrollbar;
bool didHaveVScrollbar = mHasVerticalScrollbar;
mHasHorizontalScrollbar = state.mShowHScrollbar;
mHasVerticalScrollbar = state.mShowVScrollbar;
const nsRect& newScrollPort = ScrollPort();
nsRect newScrolledAreaBounds =
if (mSkippedScrollbarLayout || reflowHScrollbar || reflowVScrollbar ||
reflowScrollCorner || HasAnyStateBits(NS_FRAME_IS_DIRTY) ||
didHaveHScrollbar != state.mShowHScrollbar ||
didHaveVScrollbar != state.mShowVScrollbar ||
didOnlyHScrollbar != mOnlyNeedHScrollbarToScrollVVInsideLV ||
didOnlyVScrollbar != mOnlyNeedVScrollbarToScrollVVInsideLV ||
!oldScrollPort.IsEqualEdges(newScrollPort) ||
!oldScrolledAreaBounds.IsEqualEdges(newScrolledAreaBounds)) {
if (!mSuppressScrollbarUpdate) {
mSkippedScrollbarLayout = false;
// place and reflow scrollbars
const nsRect insideBorderArea(
LayoutScrollbars(state, insideBorderArea, oldScrollPort);
} else {
mSkippedScrollbarLayout = true;
if (mIsRoot) {
if (RefPtr<MobileViewportManager> manager =
PresShell()->GetMobileViewportManager()) {
// Note that this runs during layout, and when we get here the root
// scrollframe has already been laid out. It may have added or removed
// scrollbars as a result of that layout, so we need to ensure the
// visual viewport is updated to account for that before we read the
// visual viewport size.
} else if (oldScrollPort.Size() != newScrollPort.Size()) {
// We want to make sure to send a visual viewport resize event if the
// scrollport changed sizes for root scroll frames. The
// MobileViewportManager will do that, but if we don't have one (ie we
// aren't a root content document for example) we have to send one
// ourselves.
if (auto* window = nsGlobalWindowInner::Cast(
aPresContext->Document()->GetInnerWindow())) {
// Note that we need to do this after the
// UpdateVisualViewportSizeForPotentialScrollbarChange call above because that
// is what updates the visual viewport size and we need it to be up to date.
if (mIsRoot && !state.OverlayScrollbars() &&
(didHaveHScrollbar != state.mShowHScrollbar ||
didHaveVScrollbar != state.mShowVScrollbar ||
didOnlyHScrollbar != mOnlyNeedHScrollbarToScrollVVInsideLV ||
didOnlyVScrollbar != mOnlyNeedVScrollbarToScrollVVInsideLV) &&
PresShell()->IsVisualViewportOffsetSet()) {
// Removing layout/classic scrollbars can make a previously valid vvoffset
// invalid. For example, if we are zoomed in on an overflow hidden document
// and then zoom back out, when apz reaches the initial resolution (ie 1.0)
// it won't know that we can remove the scrollbars, so the vvoffset can
// validly be upto the width/height of the scrollbars. After we reflow and