Source code

Revision control

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 http://mozilla.org/MPL/2.0/. */
/* rendering object to wrap rendering objects that should be scrollable */
#include "nsGfxScrollFrame.h"
#include "nsIXULRuntime.h"
#include "base/compiler_specific.h"
#include "DisplayItemClip.h"
#include "Layers.h"
#include "nsCOMPtr.h"
#include "nsIContentViewer.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 "nsBoxLayoutState.h"
#include "mozilla/dom/NodeInfo.h"
#include "nsScrollbarFrame.h"
#include "nsINode.h"
#include "nsIScrollbarMediator.h"
#include "nsITextControlFrame.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_browser.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 <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 "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 (mHelper.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 ScrollFrameHelper::ScrollEvent : public Runnable {
public:
NS_DECL_NSIRUNNABLE
explicit ScrollEvent(ScrollFrameHelper* aHelper, bool aDelayed);
void Revoke() { mHelper = nullptr; }
private:
ScrollFrameHelper* mHelper;
};
class ScrollFrameHelper::ScrollEndEvent : public Runnable {
public:
NS_DECL_NSIRUNNABLE
explicit ScrollEndEvent(ScrollFrameHelper* aHelper);
void Revoke() { mHelper = nullptr; }
private:
ScrollFrameHelper* mHelper;
};
class ScrollFrameHelper::AsyncScrollPortEvent : public Runnable {
public:
NS_DECL_NSIRUNNABLE
explicit AsyncScrollPortEvent(ScrollFrameHelper* helper)
: Runnable("ScrollFrameHelper::AsyncScrollPortEvent"), mHelper(helper) {}
void Revoke() { mHelper = nullptr; }
private:
ScrollFrameHelper* mHelper;
};
class ScrollFrameHelper::ScrolledAreaEvent : public Runnable {
public:
NS_DECL_NSIRUNNABLE
explicit ScrolledAreaEvent(ScrollFrameHelper* helper)
: Runnable("ScrollFrameHelper::ScrolledAreaEvent"), mHelper(helper) {}
void Revoke() { mHelper = nullptr; }
private:
ScrollFrameHelper* mHelper;
};
//----------------------------------------------------------------------
//----------nsHTMLScrollFrame-------------------------------------------
nsHTMLScrollFrame* NS_NewHTMLScrollFrame(PresShell* aPresShell,
ComputedStyle* aStyle, bool aIsRoot) {
return new (aPresShell)
nsHTMLScrollFrame(aStyle, aPresShell->GetPresContext(), aIsRoot);
}
NS_IMPL_FRAMEARENA_HELPERS(nsHTMLScrollFrame)
nsHTMLScrollFrame::nsHTMLScrollFrame(ComputedStyle* aStyle,
nsPresContext* aPresContext,
nsIFrame::ClassID aID, bool aIsRoot)
: nsContainerFrame(aStyle, aPresContext, aID),
mHelper(ALLOW_THIS_IN_INITIALIZER_LIST(this), aIsRoot) {}
void nsHTMLScrollFrame::ScrollbarActivityStarted() const {
if (mHelper.mScrollbarActivity) {
mHelper.mScrollbarActivity->ActivityStarted();
}
}
void nsHTMLScrollFrame::ScrollbarActivityStopped() const {
if (mHelper.mScrollbarActivity) {
mHelper.mScrollbarActivity->ActivityStopped();
}
}
nsresult nsHTMLScrollFrame::CreateAnonymousContent(
nsTArray<ContentInfo>& aElements) {
return mHelper.CreateAnonymousContent(aElements);
}
void nsHTMLScrollFrame::AppendAnonymousContentTo(
nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
mHelper.AppendAnonymousContentTo(aElements, aFilter);
}
void nsHTMLScrollFrame::DestroyFrom(nsIFrame* aDestructRoot,
PostDestroyData& aPostDestroyData) {
DestroyAbsoluteFrames(aDestructRoot, aPostDestroyData);
mHelper.Destroy(aPostDestroyData);
nsContainerFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
}
void nsHTMLScrollFrame::SetInitialChildList(ChildListID aListID,
nsFrameList& aChildList) {
nsContainerFrame::SetInitialChildList(aListID, aChildList);
mHelper.ReloadChildFrames();
}
void nsHTMLScrollFrame::AppendFrames(ChildListID aListID,
nsFrameList& aFrameList) {
NS_ASSERTION(aListID == kPrincipalList, "Only main list supported");
mFrames.AppendFrames(nullptr, aFrameList);
mHelper.ReloadChildFrames();
}
void nsHTMLScrollFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
const nsLineList::iterator* aPrevFrameLine,
nsFrameList& aFrameList) {
NS_ASSERTION(aListID == kPrincipalList, "Only main list supported");
NS_ASSERTION(!aPrevFrame || aPrevFrame->GetParent() == this,
"inserting after sibling frame with different parent");
mFrames.InsertFrames(nullptr, aPrevFrame, aFrameList);
mHelper.ReloadChildFrames();
}
void nsHTMLScrollFrame::RemoveFrame(ChildListID aListID, nsIFrame* aOldFrame) {
NS_ASSERTION(aListID == kPrincipalList, "Only main list supported");
mFrames.DestroyFrame(aOldFrame);
mHelper.ReloadChildFrames();
}
static void GetScrollbarMetrics(nsBoxLayoutState& aState, nsIFrame* aBox,
nsSize* aMin, nsSize* aPref) {
NS_ASSERTION(aState.GetRenderingContext(),
"Must have rendering context in layout state for size "
"computations");
if (aMin) {
*aMin = aBox->GetXULMinSize(aState);
nsIFrame::AddXULMargin(aBox, *aMin);
if (aMin->width < 0) {
aMin->width = 0;
}
if (aMin->height < 0) {
aMin->height = 0;
}
}
if (aPref) {
*aPref = aBox->GetXULPrefSize(aState);
nsIFrame::AddXULMargin(aBox, *aPref);
if (aPref->width < 0) {
aPref->width = 0;
}
if (aPref->height < 0) {
aPref->height = 0;
}
}
}
/**
HTML scrolling implementation
All other things being equal, we prefer layouts with fewer scrollbars showing.
*/
namespace mozilla {
enum class ShowScrollbar : uint8_t {
Auto,
Always,
// 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.
Never,
};
static ShowScrollbar ShouldShowScrollbar(StyleOverflow aOverflow) {
switch (aOverflow) {
case StyleOverflow::Scroll:
return ShowScrollbar::Always;
case StyleOverflow::Hidden:
return ShowScrollbar::Never;
default:
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;
nsBoxLayoutState mBoxState;
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
// mHelper.mScrolledFrame. The writing-mode is the same as the scroll
// container.
LogicalMargin mScrollbarGutterFromLastReflow;
// True if the most recent reflow of mHelper.mScrolledFrame is with the
// horizontal scrollbar.
bool mReflowedContentsWithHScrollbar = false;
// True if the most recent reflow of mHelper.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(nsHTMLScrollFrame* aFrame, const ReflowInput& aReflowInput);
nscoord VScrollbarMinHeight() const { return mVScrollbarMinSize.height; }
nscoord VScrollbarPrefWidth() const { return mVScrollbarPrefSize.width; }
nscoord HScrollbarMinWidth() const { return mHScrollbarMinSize.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 {
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;
}
private:
// Filled in by the constructor. Put variables here to keep them unchanged
// after initializing them in the constructor.
nsSize mVScrollbarMinSize;
nsSize mVScrollbarPrefSize;
nsSize mHScrollbarMinSize;
nsSize mHScrollbarPrefSize;
// The scrollbar gutter sizes resolved from the scrollbar-gutter and
// scrollbar-width property.
nsMargin mScrollbarGutter;
};
ScrollReflowInput::ScrollReflowInput(nsHTMLScrollFrame* aFrame,
const ReflowInput& aReflowInput)
: mReflowInput(aReflowInput),
// mBoxState is just used for scrollbars so we don't need to
// worry about the reflow depth here
mBoxState(aReflowInput.mFrame->PresContext(),
aReflowInput.mRenderingContext),
mComputedBorder(aReflowInput.ComputedPhysicalBorderPadding() -
aReflowInput.ComputedPhysicalPadding()),
mScrollbarGutterFromLastReflow(aFrame->GetWritingMode()) {
ScrollStyles styles = aFrame->GetScrollStyles();
mHScrollbar = ShouldShowScrollbar(styles.mHorizontal);
mVScrollbar = ShouldShowScrollbar(styles.mVertical);
if (nsIFrame* hScrollbarBox = aFrame->GetScrollbarBox(false)) {
nsScrollbarFrame* scrollbar = do_QueryFrame(hScrollbarBox);
scrollbar->SetScrollbarMediatorContent(mReflowInput.mFrame->GetContent());
GetScrollbarMetrics(mBoxState, hScrollbarBox, &mHScrollbarMinSize,
&mHScrollbarPrefSize);
} else {
mHScrollbar = ShowScrollbar::Never;
mHScrollbarAllowedForScrollingVVInsideLV = false;
}
if (nsIFrame* vScrollbarBox = aFrame->GetScrollbarBox(true)) {
nsScrollbarFrame* scrollbar = do_QueryFrame(vScrollbarBox);
scrollbar->SetScrollbarMediatorContent(mReflowInput.mFrame->GetContent());
GetScrollbarMetrics(mBoxState, vScrollbarBox, &mVScrollbarMinSize,
&mVScrollbarPrefSize);
} else {
mVScrollbar = ShowScrollbar::Never;
mVScrollbarAllowedForScrollingVVInsideLV = false;
}
const auto* scrollbarStyle =
nsLayoutUtils::StyleForScrollbar(mReflowInput.mFrame);
// 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.
if (scrollbarStyle->StyleUIReset()->ScrollbarWidth() ==
StyleScrollbarWidth::None) {
mHScrollbar = ShowScrollbar::Never;
mHScrollbarAllowedForScrollingVVInsideLV = false;
mVScrollbar = ShowScrollbar::Never;
mVScrollbarAllowedForScrollingVVInsideLV = false;
} else if (const auto& scrollbarGutterStyle =
scrollbarStyle->StyleDisplay()->mScrollbarGutter) {
const auto stable =
bool(scrollbarGutterStyle & StyleScrollbarGutter::STABLE);
const auto bothEdges =
bool(scrollbarGutterStyle & StyleScrollbarGutter::BOTH_EDGES);
if (mReflowInput.GetWritingMode().IsVertical()) {
const nscoord h = HScrollbarPrefHeight();
if (bothEdges) {
mScrollbarGutter.top = mScrollbarGutter.bottom = h;
} else if (stable) {
// The horizontal scrollbar gutter is always at the bottom side.
mScrollbarGutter.bottom = h;
}
} else {
const nscoord w = VScrollbarPrefWidth();
if (bothEdges) {
mScrollbarGutter.left = mScrollbarGutter.right = w;
} else if (stable) {
if (aFrame->IsScrollbarOnRight()) {
mScrollbarGutter.right = w;
} else {
mScrollbarGutter.left = w;
}
}
}
}
}
} // namespace mozilla
// XXXldb Can this go away?
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.
nscoord contentWidth = aState.mReflowInput.ComputedWidth();
if (contentWidth == NS_UNCONSTRAINEDSIZE) {
contentWidth = aDesiredInsideBorderSize.width -
aState.mReflowInput.ComputedPhysicalPadding().LeftRight();
}
nscoord contentHeight = aState.mReflowInput.ComputedHeight();
if (contentHeight == NS_UNCONSTRAINEDSIZE) {
contentHeight = aDesiredInsideBorderSize.height -
aState.mReflowInput.ComputedPhysicalPadding().TopBottom();
}
contentWidth = aState.mReflowInput.ApplyMinMaxWidth(contentWidth);
contentHeight = aState.mReflowInput.ApplyMinMaxHeight(contentHeight);
return nsSize(
contentWidth + aState.mReflowInput.ComputedPhysicalPadding().LeftRight(),
contentHeight +
aState.mReflowInput.ComputedPhysicalPadding().TopBottom());
}
/**
* 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 nsHTMLScrollFrame::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) !=
logicalScrollbarGutter.IStartEnd(wm);
const bool blockEndsGutterChanged =
aState.mScrollbarGutterFromLastReflow.BStartEnd(wm) !=
logicalScrollbarGutter.BStartEnd(wm);
const bool shouldReflowScrolledFrame =
inlineEndsGutterChanged ||
(blockEndsGutterChanged && ScrolledContentDependsOnBSize(aState));
if (shouldReflowScrolledFrame) {
if (blockEndsGutterChanged) {
nsLayoutUtils::MarkIntrinsicISizesDirtyIfDependentOnBSize(
mHelper.mScrolledFrame);
}
aKidMetrics->mOverflowAreas.Clear();
ROOT_SCROLLBAR_LOG(
"TryLayout reflowing scrolled frame with scrollbars h=%d, v=%d\n",
aAssumeHScroll, aAssumeVScroll);
ReflowScrolledFrame(aState, aAssumeHScroll, aAssumeVScroll, aKidMetrics);
}
const nsSize scrollbarGutterSize(scrollbarGutter.LeftRight(),
scrollbarGutter.TopBottom());
// First, compute our inside-border size and scrollport size
// XXXldb Can we depend more on ComputeSize here?
nsSize kidSize =
aState.mReflowInput.mStyleDisplay->GetContainSizeAxes().ContainSize(
aKidMetrics->PhysicalSize(), wm);
const nsSize desiredInsideBorderSize = kidSize + scrollbarGutterSize;
aState.mInsideBorderSize =
ComputeInsideBorderSize(aState, desiredInsideBorderSize);
nsSize layoutSize = mHelper.mIsUsingMinimumScaleSize
? mHelper.mMinimumScaleSize
: aState.mInsideBorderSize;
const nsSize scrollPortSize =
Max(nsSize(0, 0), layoutSize - scrollbarGutterSize);
if (mHelper.mIsUsingMinimumScaleSize) {
mHelper.mICBSize =
Max(nsSize(0, 0), aState.mInsideBorderSize - scrollbarGutterSize);
}
nsSize visualViewportSize = scrollPortSize;
ROOT_SCROLLBAR_LOG("TryLayout with VV %s\n",
ToString(visualViewportSize).c_str());
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 (mHelper.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",
ToString(visualViewportSize).c_str());
}
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 (mHelper.UsesOverlayScrollbars() && mHelper.mIsUsingMinimumScaleSize &&
mHelper.mMinimumScaleSize.height > overflowRect.YMost()) {
overflowRect.height +=
mHelper.mMinimumScaleSize.height - overflowRect.YMost();
}
nsRect scrolledRect =
mHelper.GetUnsnappedScrolledRectInternal(overflowRect, scrollPortSize);
ROOT_SCROLLBAR_LOG(
"TryLayout scrolledRect:%s overflowRect:%s scrollportSize:%s\n",
ToString(scrolledRect).c_str(), ToString(overflowRect).c_str(),
ToString(scrollPortSize).c_str());
nscoord oneDevPixel = aState.mBoxState.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,
aState.mComputedBorder.top + scrollbarGutter.top);
mHelper.SetScrollPort(nsRect(scrollPortOrigin, scrollPortSize));
if (mHelper.mIsRoot && gfxPlatform::UseDesktopZoomingScrollbars()) {
bool vvChanged = true;
// 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 (mHelper.ScrollPort().width >=
visualViewportSize.width + oneDevPixel &&
visualViewportSize.width >= aState.HScrollbarMinWidth()) {
vvChanged = true;
visualViewportSize.height -= aState.HScrollbarPrefHeight();
aState.mShowHScrollbar = true;
aState.mOnlyNeedHScrollbarToScrollVVInsideLV = true;
ROOT_SCROLLBAR_LOG("TryLayout added H scrollbar for VV, VV now %s\n",
ToString(visualViewportSize).c_str());
}
}
if (!aState.mShowVScrollbar &&
aState.mVScrollbarAllowedForScrollingVVInsideLV) {
if (mHelper.ScrollPort().height >=
visualViewportSize.height + oneDevPixel &&
visualViewportSize.height >= aState.VScrollbarMinHeight()) {
vvChanged = true;
visualViewportSize.width -= aState.VScrollbarPrefWidth();
aState.mShowVScrollbar = true;
aState.mOnlyNeedVScrollbarToScrollVVInsideLV = true;
ROOT_SCROLLBAR_LOG("TryLayout added V scrollbar for VV, VV now %s\n",
ToString(visualViewportSize).c_str());
}
}
}
}
return true;
}
bool nsHTMLScrollFrame::ScrolledContentDependsOnBSize(
const ScrollReflowInput& aState) const {
return mHelper.mScrolledFrame->HasAnyStateBits(
NS_FRAME_CONTAINS_RELATIVE_BSIZE |
NS_FRAME_DESCENDANT_INTRINSIC_ISIZE_DEPENDS_ON_BSIZE) ||
aState.mReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE ||
aState.mReflowInput.ComputedMinBSize() > 0 ||
aState.mReflowInput.ComputedMaxBSize() != NS_UNCONSTRAINEDSIZE;
}
void nsHTMLScrollFrame::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()) {
computedBSize = NS_UNCONSTRAINEDSIZE;
computedMinBSize = 0;
computedMaxBSize = NS_UNCONSTRAINEDSIZE;
}
const LogicalMargin scrollbarGutter(
wm, aState.ScrollbarGutter(aAssumeVScroll, aAssumeHScroll,
IsScrollbarOnRight()));
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,
mHelper.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 =
aState.mReflowInput.mFlags.mTreatBSizeAsIndefinite;
kidReflowInput.SetComputedBSize(computedBSize);
kidReflowInput.ComputedMinBSize() = computedMinBSize;
kidReflowInput.ComputedMaxBSize() = computedMaxBSize;
if (aState.mReflowInput.IsBResizeForWM(kidWM)) {
kidReflowInput.SetBResize(true);
}
if (aState.mReflowInput.IsBResizeForPercentagesForWM(kidWM)) {
kidReflowInput.mFlags.mIsBResizeForPercentages = true;
}
// Temporarily set mHasHorizontalScrollbar/mHasVerticalScrollbar to
// reflect our assumptions while we reflow the child.
bool didHaveHorizontalScrollbar = mHelper.mHasHorizontalScrollbar;
bool didHaveVerticalScrollbar = mHelper.mHasVerticalScrollbar;
mHelper.mHasHorizontalScrollbar = aAssumeHScroll;
mHelper.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(mHelper.mScrolledFrame, presContext, *aMetrics, kidReflowInput,
wm, LogicalPoint(wm), dummyContainerSize,
ReflowChildFlags::NoMoveFrame, status);
mHelper.mHasHorizontalScrollbar = didHaveHorizontalScrollbar;
mHelper.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.
FinishReflowChild(
mHelper.mScrolledFrame, presContext, *aMetrics, &kidReflowInput, wm,
LogicalPoint(wm), dummyContainerSize,
ReflowChildFlags::NoMoveFrame | ReflowChildFlags::NoSizeView);
if (mHelper.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.
AddStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE);
}
// 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.
aMetrics->UnionOverflowAreasWithDesiredBounds();
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;
mHelper.mScrolledFrame->UnionChildOverflow(childOverflow);
nsRect childScrollableOverflow = childOverflow.ScrollableOverflow();
const LogicalMargin inlinePadding =
padding.ApplySkipSides(LogicalSides(wm, eLogicalSideBitsBBoth));
childScrollableOverflow.Inflate(inlinePadding.GetPhysicalMargin(wm));
nsRect& so = aMetrics->ScrollableOverflow();
so = so.UnionEdges(childScrollableOverflow);
}
aState.mContentsOverflowAreas = aMetrics->mOverflowAreas;
aState.mScrollbarGutterFromLastReflow = scrollbarGutter;
aState.mReflowedContentsWithHScrollbar = aAssumeHScroll;
aState.mReflowedContentsWithVScrollbar = aAssumeVScroll;
}
bool nsHTMLScrollFrame::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 mHelper.mHasHorizontalScrollbar &&
!mHelper.mOnlyNeedHScrollbarToScrollVVInsideLV;
}
bool nsHTMLScrollFrame::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 (mHelper.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 mHelper.mHasVerticalScrollbar &&
!mHelper.mOnlyNeedVScrollbarToScrollVVInsideLV;
}
// If this is the initial reflow, guess false because usually
// we have very little content by then.
if (InInitialReflow()) return false;
if (mHelper.mIsRoot) {
nsIFrame* f = mHelper.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 nsHTMLScrollFrame::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 !mHelper.mIsRoot && HasAnyStateBits(NS_FRAME_FIRST_REFLOW);
}
void nsHTMLScrollFrame::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 =
aState.mReflowInput.mStyleDisplay->GetContainSizeAxes().ContainSize(
kidDesiredSize.PhysicalSize(), desiredWm);
nsSize insideBorderSize = ComputeInsideBorderSize(aState, kidSize);
nsRect scrolledRect = mHelper.GetUnsnappedScrolledRectInternal(
kidDesiredSize.ScrollableOverflow(), insideBorderSize);
if (nsRect(nsPoint(0, 0), insideBorderSize).Contains(scrolledRect)) {
// Let's pretend we had no scrollbars coming in here
kidDesiredSize.mOverflowAreas.Clear();
ReflowScrolledFrame(aState, false, false, &kidDesiredSize);
}
}
if (IsRootScrollFrameOfDocument()) {
mHelper.UpdateMinimumScaleSize(
aState.mContentsOverflowAreas.ScrollableOverflow(),
kidDesiredSize.PhysicalSize());
}
// 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",
aState.mReflowedContentsWithHScrollbar,
aState.mReflowedContentsWithVScrollbar);
if (TryLayout(aState, &kidDesiredSize, aState.mReflowedContentsWithHScrollbar,
aState.mReflowedContentsWithVScrollbar, false)) {
return;
}
ROOT_SCROLLBAR_LOG("Trying layout2 with %d, %d\n",
!aState.mReflowedContentsWithHScrollbar,
aState.mReflowedContentsWithVScrollbar);
if (TryLayout(aState, &kidDesiredSize,
!aState.mReflowedContentsWithHScrollbar,
aState.mReflowedContentsWithVScrollbar, false)) {
return;
}
// 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)) {
return;
}
ROOT_SCROLLBAR_LOG("Trying layout4 with %d, %d\n", true, newVScrollbarState);
if (TryLayout(aState, &kidDesiredSize, true, newVScrollbarState, false)) {
return;
}
// 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 nsHTMLScrollFrame::PlaceScrollArea(ScrollReflowInput& aState,
const nsPoint& aScrollPosition) {
nsIFrame* scrolledFrame = mHelper.mScrolledFrame;
// Set the x,y of the scrolled frame to the correct value
scrolledFrame->SetPosition(mHelper.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.
AdjustForPerspective(aState.mContentsOverflowAreas.ScrollableOverflow());
// Preserve the width or height of empty rects
const nsSize portSize = mHelper.ScrollPort().Size();
nsRect scrolledRect = mHelper.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);
scrolledFrame->FinishAndStoreOverflow(overflow, scrolledFrame->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 scrolledFrame won't have a view but in some cases it
// might create its own.
nsContainerFrame::SyncFrameViewAfterReflow(
scrolledFrame->PresContext(), scrolledFrame, scrolledFrame->GetView(),
scrolledArea, ReflowChildFlags::Default);
}
nscoord nsHTMLScrollFrame::IntrinsicScrollbarGutterSizeAtInlineEdges(
gfxContext* aRenderingContext) {
const bool isVerticalWM = GetWritingMode().IsVertical();
nsIFrame* inlineEndScrollbarBox =
isVerticalWM ? mHelper.mHScrollbarBox : mHelper.mVScrollbarBox;
if (!inlineEndScrollbarBox) {
// No scrollbar box frame means no intrinsic size.
return 0;
}
const auto* styleForScrollbar = nsLayoutUtils::StyleForScrollbar(this);
if (styleForScrollbar->StyleUIReset()->ScrollbarWidth() ==
StyleScrollbarWidth::None) {
// Scrollbar shouldn't appear at all with "scrollbar-width: none".
return 0;
}
const auto& styleScrollbarGutter =
styleForScrollbar->StyleDisplay()->mScrollbarGutter;
ScrollStyles ss = GetScrollStyles();
const StyleOverflow& inlineEndStyleOverflow =
isVerticalWM ? ss.mHorizontal : ss.mVertical;
// Return the scrollbar-gutter size only if we have "overflow:scroll" or
// non-auto "scrollbar-gutter", so early-return here if the conditions aren't
// satisfied.
if (inlineEndStyleOverflow != StyleOverflow::Scroll &&
styleScrollbarGutter == StyleScrollbarGutter::AUTO) {
return 0;
}
// No need to worry about reflow depth here since it's just for scrollbars.
nsBoxLayoutState bls(PresContext(), aRenderingContext, 0);
nsSize scrollbarPrefSize;
GetScrollbarMetrics(bls, inlineEndScrollbarBox, nullptr, &scrollbarPrefSize);
const nscoord scrollbarSize =
isVerticalWM ? scrollbarPrefSize.height : scrollbarPrefSize.width;
const auto bothEdges =
bool(styleScrollbarGutter & StyleScrollbarGutter::BOTH_EDGES);
return bothEdges ? scrollbarSize * 2 : scrollbarSize;
}
// Legacy, this sucks!
static bool IsMarqueeScrollbox(const nsIFrame& aScrollFrame) {
if (!aScrollFrame.GetContent()) {
return false;
}
if (MOZ_LIKELY(!aScrollFrame.GetContent()->IsInUAWidget())) {
return false;
}
MOZ_ASSERT(aScrollFrame.GetParent() &&
aScrollFrame.GetParent()->GetContent());
return aScrollFrame.GetParent() &&
HTMLMarqueeElement::FromNodeOrNull(
aScrollFrame.GetParent()->GetContent());
}
/* virtual */
nscoord nsHTMLScrollFrame::GetMinISize(gfxContext* aRenderingContext) {
nscoord result = [&] {
if (StyleDisplay()->GetContainSizeAxes().mIContained) {
return 0;
}
if (MOZ_UNLIKELY(IsMarqueeScrollbox(*this))) {
return 0;
}
return mHelper.mScrolledFrame->GetMinISize(aRenderingContext);
}();
DISPLAY_MIN_INLINE_SIZE(this, result);
return result + IntrinsicScrollbarGutterSizeAtInlineEdges(aRenderingContext);
}
/* virtual */
nscoord nsHTMLScrollFrame::GetPrefISize(gfxContext* aRenderingContext) {
nscoord result =
StyleDisplay()->GetContainSizeAxes().mIContained
? 0
: mHelper.mScrolledFrame->GetPrefISize(aRenderingContext);
DISPLAY_PREF_INLINE_SIZE(this, result);
return NSCoordSaturatingAdd(
result, IntrinsicScrollbarGutterSizeAtInlineEdges(aRenderingContext));
}
nsresult nsHTMLScrollFrame::GetXULPadding(nsMargin& aMargin) {
// Our padding hangs out on the inside of the scrollframe, but XUL doesn't
// reaize that. If we're stuck inside a XUL box, we need to claim no
// padding.
// @see also nsXULScrollFrame::GetXULPadding.
aMargin.SizeTo(0, 0, 0, 0);
return NS_OK;
}
bool nsHTMLScrollFrame::IsXULCollapsed() {
// We're never collapsed in the box sense.
return false;
}
// 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()) {
if (listID == nsIFrame::kPopupList) {
continue;
}
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);
aScrolledFrame->SetPosition(scrollPos);
}
// 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.
overhang.top /= bottomDelta;
overhang.right /= rightDelta;
overhang.bottom /= bottomDelta;
overhang.left /= 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.
overflow.Inflate(overhang);
// Merge it with the combined overflow
aScrolledFrameOverflowArea.UnionRect(aScrolledFrameOverflowArea,
overflow);
} else if (aCurrentFrame == aScrolledFrame) {
aScrolledFrameOverflowArea.UnionRect(
aScrolledFrameOverflowArea,
child->ScrollableOverflowRectRelativeToParent());
}
}
}
}
nscoord nsHTMLScrollFrame::GetLogicalBaseline(WritingMode aWritingMode) const {
// This function implements some of the spec text here:
//
// Specifically: if our scrolled frame is a block, we just use the inherited
// GetLogicalBaseline() impl, which synthesizes a baseline from the
// margin-box. Otherwise, we defer to our scrolled frame, considering it
// to be scrolled to its initial scroll position.
if (mHelper.mScrolledFrame->IsBlockFrameOrSubclass() ||
StyleDisplay()->IsContainLayout()) {
return nsContainerFrame::GetLogicalBaseline(aWritingMode);
}
// OK, here's where we defer to our scrolled frame. 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(aWritingMode);
return border.BStart(aWritingMode) +
mHelper.mScrolledFrame->GetLogicalBaseline(aWritingMode);
}
void nsHTMLScrollFrame::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()) {
return;
}
aScrollableOverflow.SetEmpty();
GetScrollableOverflowForPerspective(
mHelper.mScrolledFrame, mHelper.mScrolledFrame, mHelper.ScrollPort(),
nsPoint(), aScrollableOverflow);
}
void nsHTMLScrollFrame::Reflow(nsPresContext* aPresContext,
ReflowOutput& aDesiredSize,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
MarkInReflow();
DO_GLOBAL_REFLOW_COUNT("nsHTMLScrollFrame");
DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
mHelper.HandleScrollbarStyleSwitching();
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(mHelper.mHScrollbarBox);
reflowVScrollbar = NeedsReflow(mHelper.mVScrollbarBox);
reflowScrollCorner = NeedsReflow(mHelper.mScrollCornerBox) ||
NeedsReflow(mHelper.mResizerBox);
}
if (mHelper.mIsRoot) {
reflowScrollCorner = false;
}
const nsRect oldScrollPort = mHelper.ScrollPort();
nsRect oldScrolledAreaBounds =
mHelper.mScrolledFrame->ScrollableOverflowRectRelativeToParent();
nsPoint oldScrollPosition = mHelper.GetScrollPosition();
ReflowContents(state, aDesiredSize);
nsSize layoutSize = mHelper.mIsUsingMinimumScaleSize
? mHelper.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.
SetSize(aDesiredSize.GetWritingMode(),
aDesiredSize.Size(aDesiredSize.GetWritingMode()));
// 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 (!mHelper.mPostedReflowCallback) {
// Make sure we'll try scrolling to restored position
PresShell()->PostReflowCallback(&mHelper);
mHelper.mPostedReflowCallback = true;
}
bool didOnlyHScrollbar = mHelper.mOnlyNeedHScrollbarToScrollVVInsideLV;
bool didOnlyVScrollbar = mHelper.mOnlyNeedVScrollbarToScrollVVInsideLV;
mHelper.mOnlyNeedHScrollbarToScrollVVInsideLV =
state.mOnlyNeedHScrollbarToScrollVVInsideLV;
mHelper.mOnlyNeedVScrollbarToScrollVVInsideLV =
state.mOnlyNeedVScrollbarToScrollVVInsideLV;
bool didHaveHScrollbar = mHelper.mHasHorizontalScrollbar;
bool didHaveVScrollbar = mHelper.mHasVerticalScrollbar;
mHelper.mHasHorizontalScrollbar = state.mShowHScrollbar;
mHelper.mHasVerticalScrollbar = state.mShowVScrollbar;
const nsRect& newScrollPort = mHelper.ScrollPort();
nsRect newScrolledAreaBounds =
mHelper.mScrolledFrame->ScrollableOverflowRectRelativeToParent();
if (mHelper.mSkippedScrollbarLayout || reflowHScrollbar || reflowVScrollbar ||
reflowScrollCorner || HasAnyStateBits(NS_FRAME_IS_DIRTY) ||
didHaveHScrollbar != state.mShowHScrollbar ||
didHaveVScrollbar != state.mShowVScrollbar ||
didOnlyHScrollbar != mHelper.mOnlyNeedHScrollbarToScrollVVInsideLV ||
didOnlyVScrollbar != mHelper.mOnlyNeedVScrollbarToScrollVVInsideLV ||
!oldScrollPort.IsEqualEdges(newScrollPort) ||
!oldScrolledAreaBounds.IsEqualEdges(newScrolledAreaBounds)) {
if (!mHelper.mSuppressScrollbarUpdate) {
mHelper.mSkippedScrollbarLayout = false;
ScrollFrameHelper::SetScrollbarVisibility(mHelper.mHScrollbarBox,
state.mShowHScrollbar);
ScrollFrameHelper::SetScrollbarVisibility(mHelper.mVScrollbarBox,
state.mShowVScrollbar);
// place and reflow scrollbars
const nsRect insideBorderArea(
nsPoint(state.mComputedBorder.left, state.mComputedBorder.top),
layoutSize);
mHelper.LayoutScrollbars(state.mBoxState, insideBorderArea,
oldScrollPort);
} else {
mHelper.mSkippedScrollbarLayout = true;
}
}
if (mHelper.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.
manager->UpdateVisualViewportSizeForPotentialScrollbarChange();
} 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())) {
window->VisualViewport()->PostResizeEvent();
}
}
}
// 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 (mHelper.mIsRoot && !mHelper.UsesOverlayScrollbars() &&
(didHaveHScrollbar != state.mShowHScrollbar ||
didHaveVScrollbar != state.mShowVScrollbar ||
didOnlyHScrollbar != mHelper.mOnlyNeedHScrollbarToScrollVVInsideLV ||
didOnlyVScrollbar != mHelper.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
// remove the scrollbars the only valid vvoffset is (0,0). We could wait
// until we send the new frame metrics to apz and then have it reply with
// the new corrected vvoffset but having an inconsistent vvoffset causes
// problems so trigger the vvoffset to be re-set and re-clamped in
// ReflowFinished.
mHelper.mReclampVVOffsetInReflowFinished = true;
}
aDesiredSize.SetOverflowAreasToDesiredBounds();
mHelper.UpdateSticky();
FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput,
aStatus);
if (!InInitialReflow() && !mHelper.mHadNonInitialReflow) {
mHelper.mHadNonInitialReflow = true;
}
if (mHelper.mIsRoot &&
!oldScrolledAreaBounds.IsEqualEdges(newScrolledAreaBounds)) {
mHelper.PostScrolledAreaEvent();
}
mHelper.UpdatePrevScrolledRect();
aStatus.Reset(); // This type of frame can't be split.
NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize);
mHelper.PostOverflowEvent();
}
void nsHTMLScrollFrame::DidReflow(nsPresContext* aPresContext,
const ReflowInput* aReflowInput) {
nsContainerFrame::DidReflow(aPresContext, aReflowInput);
PresShell()->PostPendingScrollAnchorAdjustment(Anchor());
}
////////////////////////////////////////////////////////////////////////////////
#ifdef DEBUG_FRAME_DUMP
nsresult nsHTMLScrollFrame::GetFrameName(nsAString& aResult) const {
return