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 "ActiveLayerTracker.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/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 "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/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 <stdint.h>
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Telemetry.h"
#include "FrameLayerBuilder.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/LayerTransactionChild.h"
#include "mozilla/layers/ScrollLinkedEffectDetector.h"
#include "mozilla/Unused.h"
#include "MobileViewportManager.h"
#include "VisualViewport.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();
}
/**
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 {
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;
MOZ_INIT_OUTSIDE_CTOR
bool mReflowedContentsWithHScrollbar;
MOZ_INIT_OUTSIDE_CTOR
bool mReflowedContentsWithVScrollbar;
// === Filled in when TryLayout succeeds ===
// The size of the inside-border area
nsSize mInsideBorderSize;
// Whether we decided to show the horizontal scrollbar
MOZ_INIT_OUTSIDE_CTOR
bool mShowHScrollbar;
// Whether we decided to show the vertical scrollbar
MOZ_INIT_OUTSIDE_CTOR
bool mShowVScrollbar;
// 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(nsIScrollableFrame* 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) {
ScrollStyles styles = aFrame->GetScrollStyles();
mHScrollbar = ShouldShowScrollbar(styles.mHorizontal);
mVScrollbar = ShouldShowScrollbar(styles.mVertical);
}
};
} // namespace mozilla
// XXXldb Can this go away?
static nsSize ComputeInsideBorderSize(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());
}
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;
}
}
}
/**
* 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;
}
if (aAssumeVScroll != aState->mReflowedContentsWithVScrollbar ||
(aAssumeHScroll != aState->mReflowedContentsWithHScrollbar &&
ScrolledContentDependsOnHeight(aState))) {
if (aAssumeHScroll != aState->mReflowedContentsWithHScrollbar) {
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);
}
nsSize vScrollbarMinSize(0, 0);
nsSize vScrollbarPrefSize(0, 0);
if (mHelper.mVScrollbarBox) {
GetScrollbarMetrics(aState->mBoxState, mHelper.mVScrollbarBox,
&vScrollbarMinSize, &vScrollbarPrefSize);
nsScrollbarFrame* scrollbar = do_QueryFrame(mHelper.mVScrollbarBox);
scrollbar->SetScrollbarMediatorContent(mContent);
}
nscoord vScrollbarDesiredWidth =
aAssumeVScroll ? vScrollbarPrefSize.width : 0;
nsSize hScrollbarMinSize(0, 0);
nsSize hScrollbarPrefSize(0, 0);
if (mHelper.mHScrollbarBox) {
GetScrollbarMetrics(aState->mBoxState, mHelper.mHScrollbarBox,
&hScrollbarMinSize, &hScrollbarPrefSize);
nsScrollbarFrame* scrollbar = do_QueryFrame(mHelper.mHScrollbarBox);
scrollbar->SetScrollbarMediatorContent(mContent);
}
nscoord hScrollbarDesiredHeight =
aAssumeHScroll ? hScrollbarPrefSize.height : 0;
// First, compute our inside-border size and scrollport size
// XXXldb Can we depend more on ComputeSize here?
nsSize kidSize = aState->mReflowInput.mStyleDisplay->IsContainSize()
? nsSize(0, 0)
: aKidMetrics->PhysicalSize();
nsSize desiredInsideBorderSize;
desiredInsideBorderSize.width = vScrollbarDesiredWidth + kidSize.width;
desiredInsideBorderSize.height = hScrollbarDesiredHeight + kidSize.height;
aState->mInsideBorderSize =
ComputeInsideBorderSize(aState, desiredInsideBorderSize);
nsSize layoutSize = mHelper.mIsUsingMinimumScaleSize
? mHelper.mMinimumScaleSize
: aState->mInsideBorderSize;
const nsSize scrollPortSize =
nsSize(std::max(0, layoutSize.width - vScrollbarDesiredWidth),
std::max(0, layoutSize.height - hScrollbarDesiredHeight));
if (mHelper.mIsUsingMinimumScaleSize) {
mHelper.mICBSize = nsSize(
std::max(0, aState->mInsideBorderSize.width - vScrollbarDesiredWidth),
std::max(0,
aState->mInsideBorderSize.height - hScrollbarDesiredHeight));
}
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 = nsSize(
std::max(0, visualViewportSize.width - vScrollbarDesiredWidth),
std::max(0, visualViewportSize.height - hScrollbarDesiredHeight));
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);
if (!aForce) {
nsSize sizeToCompare = visualViewportSize;
if (gfxPlatform::UseDesktopZoomingScrollbars()) {
sizeToCompare = scrollPortSize;
}
// If the style is HIDDEN then we already know that aAssumeHScroll is false
if (aState->mHScrollbar != ShowScrollbar::Never) {
bool wantHScrollbar =
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 < hScrollbarMinSize.width) {
wantHScrollbar = false;
}
ROOT_SCROLLBAR_LOG("TryLayout wants H Scrollbar: %d =? %d\n",
wantHScrollbar, aAssumeHScroll);
if (wantHScrollbar != aAssumeHScroll) {
return false;
}
}
// If the style is HIDDEN then we already know that aAssumeVScroll is false
if (aState->mVScrollbar != ShowScrollbar::Never) {
bool wantVScrollbar =
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 < vScrollbarMinSize.height) {
wantVScrollbar = false;
}
ROOT_SCROLLBAR_LOG("TryLayout wants V Scrollbar: %d =? %d\n",
wantVScrollbar, aAssumeVScroll);
if (wantVScrollbar != aAssumeVScroll) {
return false;
}
}
}
aState->mShowHScrollbar = aAssumeHScroll;
aState->mShowVScrollbar = aAssumeVScroll;
nsPoint scrollPortOrigin(aState->mComputedBorder.left,
aState->mComputedBorder.top);
if (!IsScrollbarOnRight()) {
nscoord vScrollbarActualWidth = layoutSize.width - scrollPortSize.width;
scrollPortOrigin.x += vScrollbarActualWidth;
}
mHelper.mScrollPort = 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.mScrollPort.width >=
visualViewportSize.width + oneDevPixel &&
visualViewportSize.width >= hScrollbarMinSize.width) {
vvChanged = true;
visualViewportSize.height -= hScrollbarPrefSize.height;
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.mScrollPort.height >=
visualViewportSize.height + oneDevPixel &&
visualViewportSize.height >= vScrollbarMinSize.height) {
vvChanged = true;
visualViewportSize.width -= vScrollbarPrefSize.width;
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;
}
// XXX Height/BSize mismatch needs to be addressed here; check the caller!
// Currently this will only behave as expected for horizontal writing modes.
// (See bug 1175509.)
bool nsHTMLScrollFrame::ScrolledContentDependsOnHeight(
ScrollReflowInput* aState) {
// Return true if ReflowScrolledFrame is going to do something different
// based on the presence of a horizontal scrollbar.
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) {
WritingMode wm = mHelper.mScrolledFrame->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;
}
if (wm.IsVertical()) {
if (aAssumeVScroll) {
nsSize vScrollbarPrefSize;
GetScrollbarMetrics(aState->mBoxState, mHelper.mVScrollbarBox, nullptr,
&vScrollbarPrefSize);
if (computedBSize != NS_UNCONSTRAINEDSIZE) {
computedBSize = std::max(0, computedBSize - vScrollbarPrefSize.width);
}
computedMinBSize =
std::max(0, computedMinBSize - vScrollbarPrefSize.width);
if (computedMaxBSize != NS_UNCONSTRAINEDSIZE) {
computedMaxBSize =
std::max(0, computedMaxBSize - vScrollbarPrefSize.width);
}
}
if (aAssumeHScroll) {
nsSize hScrollbarPrefSize;
GetScrollbarMetrics(aState->mBoxState, mHelper.mHScrollbarBox, nullptr,
&hScrollbarPrefSize);
availISize = std::max(0, availISize - hScrollbarPrefSize.height);
}
} else {
if (aAssumeHScroll) {
nsSize hScrollbarPrefSize;
GetScrollbarMetrics(aState->mBoxState, mHelper.mHScrollbarBox, nullptr,
&hScrollbarPrefSize);
if (computedBSize != NS_UNCONSTRAINEDSIZE) {
computedBSize = std::max(0, computedBSize - hScrollbarPrefSize.height);
}
computedMinBSize =
std::max(0, computedMinBSize - hScrollbarPrefSize.height);
if (computedMaxBSize != NS_UNCONSTRAINEDSIZE) {
computedMaxBSize =
std::max(0, computedMaxBSize - hScrollbarPrefSize.height);
}
}
if (aAssumeVScroll) {
nsSize vScrollbarPrefSize;
GetScrollbarMetrics(aState->mBoxState, mHelper.mVScrollbarBox, nullptr,
&vScrollbarPrefSize);
availISize = std::max(0, availISize - vScrollbarPrefSize.width);
}
}
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.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);
// 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)) {
// If the scrolled frame can be scrolled in the inline axis, inflate its
// scrollable overflow areas with its inline-end padding to prevent its
// content from being clipped at scroll container's inline-end padding
// edge.
//
// Note: Inflating scrolled frame's overflow areas is generally wrong if the
// scrolled frame's children themselves has any scrollable overflow areas.
// However, we can only be here in production for <textarea> and <input>.
// Both elements can only have text children, which shouldn't have
// scrollable overflow areas themselves, so its fine.
nsRect& so = aMetrics->ScrollableOverflow();
const nscoord soInlineSize = wm.IsVertical() ? so.Height() : so.Width();
if (soInlineSize > availISize) {
const LogicalMargin inlinePaddingEnd =
padding.ApplySkipSides(LogicalSides(wm, eLogicalSideBitsBBoth) |
LogicalSides(wm, eLogicalSideBitsIStart));
so.Inflate(inlinePaddingEnd.GetPhysicalMargin(wm));
}
}
aState->mContentsOverflowAreas = aMetrics->mOverflowAreas;
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) {
ReflowOutput kidDesiredSize(aDesiredSize.GetWritingMode());
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->IsContainSize()
? nsSize(0, 0)
: kidDesiredSize.PhysicalSize();
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.mScrollPort.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
nsSize portSize = mHelper.mScrollPort.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::GetIntrinsicVScrollbarWidth(
gfxContext* aRenderingContext) {
ScrollStyles ss = GetScrollStyles();
if (ss.mVertical != StyleOverflow::Scroll || !mHelper.mVScrollbarBox)
return 0;
// Don't need to worry about reflow depth here since it's
// just for scrollbars
nsBoxLayoutState bls(PresContext(), aRenderingContext, 0);
nsSize vScrollbarPrefSize(0, 0);
GetScrollbarMetrics(bls, mHelper.mVScrollbarBox, nullptr,
&vScrollbarPrefSize);
return vScrollbarPrefSize.width;
}
// 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()->IsContainSize()) {
return 0;
}
if (MOZ_UNLIKELY(IsMarqueeScrollbox(*this))) {
return 0;
}
return mHelper.mScrolledFrame->GetMinISize(aRenderingContext);
}();
DISPLAY_MIN_INLINE_SIZE(this, result);
return result + GetIntrinsicVScrollbarWidth(aRenderingContext);
}
/* virtual */
nscoord nsHTMLScrollFrame::GetPrefISize(gfxContext* aRenderingContext) {
nscoord result =
StyleDisplay()->IsContainSize()
? 0
: mHelper.mScrolledFrame->GetPrefISize(aRenderingContext);
DISPLAY_PREF_INLINE_SIZE(this, result);
return NSCoordSaturatingAdd(result,
GetIntrinsicVScrollbarWidth(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.
FrameChildListIDs skip = {nsIFrame::kSelectPopupList, nsIFrame::kPopupList};
for (const auto& [list, listID] : aCurrentFrame->ChildLists()) {
if (skip.contains(listID)) {
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.mScrollPort,
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);
// sanity check: ensure that if we have no scrollbar, we treat it
// as hidden.
if (!mHelper.mVScrollbarBox || mHelper.mNeverHasVerticalScrollbar) {
state.mVScrollbarAllowedForScrollingVVInsideLV = false;
state.mVScrollbar = ShowScrollbar::Never;
}
if (!mHelper.mHScrollbarBox || mHelper.mNeverHasHorizontalScrollbar) {
state.mHScrollbarAllowedForScrollingVVInsideLV = false;
state.mHScrollbar = ShowScrollbar::Never;
}
//------------ Handle Incremental Reflow -----------------
bool reflowHScrollbar = true;
bool reflowVScrollbar = true;
bool reflowScrollCorner = true;
if (!aReflowInput.ShouldReflowAllKids()) {
#define NEEDS_REFLOW(frame_) ((frame_) && (frame_)->IsSubtreeDirty())
reflowHScrollbar = NEEDS_REFLOW(mHelper.mHScrollbarBox);
reflowVScrollbar = NEEDS_REFLOW(mHelper.mVScrollbarBox);
reflowScrollCorner = NEEDS_REFLOW(mHelper.mScrollCornerBox) ||
NEEDS_REFLOW(mHelper.mResizerBox);
#undef NEEDS_REFLOW
}
if (mHelper.mIsRoot) {
reflowScrollCorner = false;
// Hide the scrollbar when the scrollbar-width is set to none.
// This is only needed for root element because scrollbars of non-
// root elements with "scrollbar-width: none" is already suppressed
// in ScrollFrameHelper::CreateAnonymousContent.
ComputedStyle* scrollbarStyle = nsLayoutUtils::StyleForScrollbar(this);
auto scrollbarWidth = scrollbarStyle->StyleUIReset()->mScrollbarWidth;
if (scrollbarWidth == StyleScrollbarWidth::None) {
state.mVScrollbarAllowedForScrollingVVInsideLV = false;
state.mHScrollbarAllowedForScrollingVVInsideLV = false;
state.mVScrollbar = ShowScrollbar::Never;
state.mHScrollbar = ShowScrollbar::Never;
}
}
nsRect oldScrollAreaBounds = mHelper.mScrollPort;
nsRect oldScrolledAreaBounds =
mHelper.mScrolledFrame->ScrollableOverflowRectRelativeToParent();
nsPoint oldScrollPosition = mHelper.GetScrollPosition();
state.mComputedBorder = aReflowInput.ComputedPhysicalBorderPadding() -
aReflowInput.ComputedPhysicalPadding();
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;
nsRect newScrollAreaBounds = mHelper.mScrollPort;
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 ||
!oldScrollAreaBounds.IsEqualEdges(newScrollAreaBounds) ||
!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
nsRect insideBorderArea =
nsRect(nsPoint(state.mComputedBorder.left, state.mComputedBorder.top),
layoutSize);
mHelper.LayoutScrollbars(state.mBoxState, insideBorderArea,
oldScrollAreaBounds);
} 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();
}
}
// 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)) {
// 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 MakeFrameName(u"HTMLScroll"_ns, aResult);
}
#endif
#ifdef ACCESSIBILITY
a11y::AccType nsHTMLScrollFrame::AccessibleType() {
if (IsTableCaption()) {
return GetRect().IsEmpty() ? a11y::eNoType : a11y::eHTMLCaptionType;
}
// Create an accessible regardless of focusable state because the state can be
// changed during frame life cycle without any notifications to accessibility.
if (mContent->IsRootOfNativeAnonymousSubtree() ||
GetScrollStyles().IsHiddenInBothDirections()) {
return a11y::eNoType;
}
return a11y::eHyperTextType;
}
#endif
NS_QUERYFRAME_HEAD(nsHTMLScrollFrame)
NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
NS_QUERYFRAME_ENTRY(nsIScrollableFrame)
NS_QUERYFRAME_ENTRY(nsIStatefulFrame)
NS_QUERYFRAME_ENTRY(nsIScrollbarMediator)
NS_QUERYFRAME_ENTRY(nsHTMLScrollFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
//----------nsXULScrollFrame-------------------------------------------
nsXULScrollFrame* NS_NewXULScrollFrame(PresShell* aPresShell,
ComputedStyle* aStyle, bool aIsRoot,
bool aClipAllDescendants) {
return new (aPresShell) nsXULScrollFrame(aStyle, aPresShell->GetPresContext(),
aIsRoot, aClipAllDescendants);
}
NS_IMPL_FRAMEARENA_HELPERS(nsXULScrollFrame)
nsXULScrollFrame::nsXULScrollFrame(ComputedStyle* aStyle,
nsPresContext* aPresContext, bool aIsRoot,
bool aClipAllDescendants)
: nsBoxFrame(aStyle, aPresContext, kClassID, aIsRoot),
mHelper(ALLOW_THIS_IN_INITIALIZER_LIST(this), aIsRoot) {
SetXULLayoutManager(nullptr);
mHelper.mClipAllDescendants = aClipAllDescendants;
}
void nsXULScrollFrame::ScrollbarActivityStarted() const {
if (mHelper.mScrollbarActivity) {
mHelper.mScrollbarActivity->ActivityStarted();
}
}
void nsXULScrollFrame::ScrollbarActivityStopped() const {
if (mHelper.mScrollbarActivity) {
mHelper.mScrollbarActivity->ActivityStopped();
}
}
nsMargin ScrollFrameHelper::GetDesiredScrollbarSizes(nsBoxLayoutState* aState) {
NS_ASSERTION(aState && aState->GetRenderingContext(),
"Must have rendering context in layout state for size "
"computations");
nsMargin result(0, 0, 0, 0);
if (mVScrollbarBox) {
nsSize size = mVScrollbarBox->GetXULPrefSize(*aState);
nsIFrame::AddXULMargin(mVScrollbarBox, size);
if (IsScrollbarOnRight())
result.left = size.width;
else
result.right = size.width;
}
if (mHScrollbarBox) {
nsSize size = mHScrollbarBox->GetXULPrefSize(*aState);
nsIFrame::AddXULMargin(mHScrollbarBox, size);
// We don't currently support any scripts that would require a scrollbar
// at the top. (Are there any?)
result.bottom = size.height;