diff options
Diffstat (limited to 'layout/generic/nsGfxScrollFrame.cpp')
-rw-r--r-- | layout/generic/nsGfxScrollFrame.cpp | 6162 |
1 files changed, 6162 insertions, 0 deletions
diff --git a/layout/generic/nsGfxScrollFrame.cpp b/layout/generic/nsGfxScrollFrame.cpp new file mode 100644 index 000000000..f664845b6 --- /dev/null +++ b/layout/generic/nsGfxScrollFrame.cpp @@ -0,0 +1,6162 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "ActiveLayerTracker.h" +#include "base/compiler_specific.h" +#include "DisplayItemClip.h" +#include "DisplayItemScrollClip.h" +#include "nsCOMPtr.h" +#include "nsIContentViewer.h" +#include "nsPresContext.h" +#include "nsView.h" +#include "nsIScrollable.h" +#include "nsContainerFrame.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsContentList.h" +#include "nsIDocumentInlines.h" +#include "nsFontMetrics.h" +#include "nsBoxLayoutState.h" +#include "mozilla/dom/NodeInfo.h" +#include "nsScrollbarFrame.h" +#include "nsIScrollbarMediator.h" +#include "nsITextControlFrame.h" +#include "nsIDOMHTMLTextAreaElement.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsPresState.h" +#include "nsIHTMLDocument.h" +#include "nsContentUtils.h" +#include "nsLayoutUtils.h" +#include "nsBidiPresUtils.h" +#include "nsBidiUtils.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Preferences.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/dom/Element.h" +#include <stdint.h> +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Telemetry.h" +#include "FrameLayerBuilder.h" +#include "nsSMILKeySpline.h" +#include "nsSubDocumentFrame.h" +#include "nsSVGOuterSVGFrame.h" +#include "nsIObjectLoadingContent.h" +#include "mozilla/Attributes.h" +#include "ScrollbarActivity.h" +#include "nsRefreshDriver.h" +#include "nsThemeConstants.h" +#include "nsSVGIntegrationUtils.h" +#include "nsIScrollPositionListener.h" +#include "StickyScrollContainer.h" +#include "nsIFrameInlines.h" +#include "gfxPlatform.h" +#include "gfxPrefs.h" +#include "AsyncScrollBase.h" +#include "ScrollSnap.h" +#include "UnitTransforms.h" +#include "nsPluginFrame.h" +#include "mozilla/layers/APZCCallbackHelper.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 "LayersLogging.h" // for Stringify +#include <algorithm> +#include <cstdlib> // for std::abs(int/long) +#include <cmath> // for std::abs(float/double) + +#define PAINT_SKIP_LOG(...) +// #define PAINT_SKIP_LOG(...) printf_stderr("PSKIP: " __VA_ARGS__) + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::layers; +using namespace mozilla::layout; + +static uint32_t +GetOverflowChange(const nsRect& aCurScrolledRect, const nsRect& aPrevScrolledRect) +{ + uint32_t result = 0; + if (aPrevScrolledRect.x != aCurScrolledRect.x || + aPrevScrolledRect.width != aCurScrolledRect.width) { + result |= nsIScrollableFrame::HORIZONTAL; + } + if (aPrevScrolledRect.y != aCurScrolledRect.y || + aPrevScrolledRect.height != aCurScrolledRect.height) { + result |= nsIScrollableFrame::VERTICAL; + } + return result; +} + +//---------------------------------------------------------------------- + +//----------nsHTMLScrollFrame------------------------------------------- + +nsHTMLScrollFrame* +NS_NewHTMLScrollFrame(nsIPresShell* aPresShell, nsStyleContext* aContext, bool aIsRoot) +{ + return new (aPresShell) nsHTMLScrollFrame(aContext, aIsRoot); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsHTMLScrollFrame) + +nsHTMLScrollFrame::nsHTMLScrollFrame(nsStyleContext* aContext, bool aIsRoot) + : nsContainerFrame(aContext), + 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) +{ + DestroyAbsoluteFrames(aDestructRoot); + mHelper.Destroy(); + nsContainerFrame::DestroyFrom(aDestructRoot); +} + +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, + 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(); +} + +nsSplittableType +nsHTMLScrollFrame::GetSplittableType() const +{ + return NS_FRAME_NOT_SPLITTABLE; +} + +nsIAtom* +nsHTMLScrollFrame::GetType() const +{ + return nsGkAtoms::scrollFrame; +} + +/** + HTML scrolling implementation + + All other things being equal, we prefer layouts with fewer scrollbars showing. +*/ + +namespace mozilla { + +struct MOZ_STACK_CLASS ScrollReflowInput { + const ReflowInput& mReflowInput; + nsBoxLayoutState mBoxState; + ScrollbarStyles mStyles; + nsMargin mComputedBorder; + + // === Filled in by ReflowScrolledFrame === + nsOverflowAreas 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; + + ScrollReflowInput(nsIScrollableFrame* aFrame, + const ReflowInput& aState) : + mReflowInput(aState), + // mBoxState is just used for scrollbars so we don't need to + // worry about the reflow depth here + mBoxState(aState.mFrame->PresContext(), aState.mRenderingContext, 0), + mStyles(aFrame->GetScrollbarStyles()) { + } +}; + +} // 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, bool aVertical) +{ + NS_ASSERTION(aState.GetRenderingContext(), + "Must have rendering context in layout state for size " + "computations"); + + if (aMin) { + *aMin = aBox->GetXULMinSize(aState); + nsBox::AddMargin(aBox, *aMin); + if (aMin->width < 0) { + aMin->width = 0; + } + if (aMin->height < 0) { + aMin->height = 0; + } + } + + if (aPref) { + *aPref = aBox->GetXULPrefSize(aState); + nsBox::AddMargin(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) our scrollport width (the inside-border width minus the width allocated for a + * vertical scrollbar, if showing) is at least the scrollbar's min-width + * (i.e., the scrollbar fits horizontally) + * 4) 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->mStyles.mVertical == NS_STYLE_OVERFLOW_HIDDEN && aAssumeVScroll) || + (aState->mStyles.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN && 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); + } + ReflowScrolledFrame(aState, aAssumeHScroll, aAssumeVScroll, aKidMetrics, + false); + } + + nsSize vScrollbarMinSize(0, 0); + nsSize vScrollbarPrefSize(0, 0); + if (mHelper.mVScrollbarBox) { + GetScrollbarMetrics(aState->mBoxState, mHelper.mVScrollbarBox, + &vScrollbarMinSize, + aAssumeVScroll ? &vScrollbarPrefSize : nullptr, true); + nsScrollbarFrame* scrollbar = do_QueryFrame(mHelper.mVScrollbarBox); + scrollbar->SetScrollbarMediatorContent(mContent); + } + nscoord vScrollbarDesiredWidth = aAssumeVScroll ? vScrollbarPrefSize.width : 0; + nscoord vScrollbarMinHeight = aAssumeVScroll ? vScrollbarMinSize.height : 0; + + nsSize hScrollbarMinSize(0, 0); + nsSize hScrollbarPrefSize(0, 0); + if (mHelper.mHScrollbarBox) { + GetScrollbarMetrics(aState->mBoxState, mHelper.mHScrollbarBox, + &hScrollbarMinSize, + aAssumeHScroll ? &hScrollbarPrefSize : nullptr, false); + nsScrollbarFrame* scrollbar = do_QueryFrame(mHelper.mHScrollbarBox); + scrollbar->SetScrollbarMediatorContent(mContent); + } + nscoord hScrollbarDesiredHeight = aAssumeHScroll ? hScrollbarPrefSize.height : 0; + nscoord hScrollbarMinWidth = aAssumeHScroll ? hScrollbarMinSize.width : 0; + + // First, compute our inside-border size and scrollport size + // XXXldb Can we depend more on ComputeSize here? + nsSize desiredInsideBorderSize; + desiredInsideBorderSize.width = vScrollbarDesiredWidth + + std::max(aKidMetrics->Width(), hScrollbarMinWidth); + desiredInsideBorderSize.height = hScrollbarDesiredHeight + + std::max(aKidMetrics->Height(), vScrollbarMinHeight); + aState->mInsideBorderSize = + ComputeInsideBorderSize(aState, desiredInsideBorderSize); + nsSize scrollPortSize = nsSize(std::max(0, aState->mInsideBorderSize.width - vScrollbarDesiredWidth), + std::max(0, aState->mInsideBorderSize.height - hScrollbarDesiredHeight)); + + nsSize visualScrollPortSize = scrollPortSize; + nsIPresShell* presShell = PresContext()->PresShell(); + if (mHelper.mIsRoot && presShell->IsScrollPositionClampingScrollPortSizeSet()) { + nsSize compositionSize = nsLayoutUtils::CalculateCompositionSizeForFrame(this, false); + float resolution = presShell->GetResolution(); + compositionSize.width /= resolution; + compositionSize.height /= resolution; + visualScrollPortSize = nsSize(std::max(0, compositionSize.width - vScrollbarDesiredWidth), + std::max(0, compositionSize.height - hScrollbarDesiredHeight)); + } + + if (!aForce) { + nsRect scrolledRect = + mHelper.GetUnsnappedScrolledRectInternal(aState->mContentsOverflowAreas.ScrollableOverflow(), + scrollPortSize); + nscoord oneDevPixel = aState->mBoxState.PresContext()->DevPixelsToAppUnits(1); + + // If the style is HIDDEN then we already know that aAssumeHScroll is false + if (aState->mStyles.mHorizontal != NS_STYLE_OVERFLOW_HIDDEN) { + bool wantHScrollbar = + aState->mStyles.mHorizontal == NS_STYLE_OVERFLOW_SCROLL || + scrolledRect.XMost() >= visualScrollPortSize.width + oneDevPixel || + scrolledRect.x <= -oneDevPixel; + if (scrollPortSize.width < hScrollbarMinSize.width) + wantHScrollbar = false; + if (wantHScrollbar != aAssumeHScroll) + return false; + } + + // If the style is HIDDEN then we already know that aAssumeVScroll is false + if (aState->mStyles.mVertical != NS_STYLE_OVERFLOW_HIDDEN) { + bool wantVScrollbar = + aState->mStyles.mVertical == NS_STYLE_OVERFLOW_SCROLL || + scrolledRect.YMost() >= visualScrollPortSize.height + oneDevPixel || + scrolledRect.y <= -oneDevPixel; + if (scrollPortSize.height < vScrollbarMinSize.height) + wantVScrollbar = false; + if (wantVScrollbar != aAssumeVScroll) + return false; + } + } + + nscoord vScrollbarActualWidth = aState->mInsideBorderSize.width - scrollPortSize.width; + + aState->mShowHScrollbar = aAssumeHScroll; + aState->mShowVScrollbar = aAssumeVScroll; + nsPoint scrollPortOrigin(aState->mComputedBorder.left, + aState->mComputedBorder.top); + if (!IsScrollbarOnRight()) { + scrollPortOrigin.x += vScrollbarActualWidth; + } + mHelper.mScrollPort = nsRect(scrollPortOrigin, scrollPortSize); + 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, + bool aFirstPass) +{ + WritingMode wm = mHelper.mScrolledFrame->GetWritingMode(); + + // these could be NS_UNCONSTRAINEDSIZE ... std::min arithmetic should + // be OK + LogicalMargin padding = aState->mReflowInput.ComputedLogicalPadding(); + 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, false); + 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, true); + availISize = std::max(0, availISize - hScrollbarPrefSize.height); + } + } else { + if (aAssumeHScroll) { + nsSize hScrollbarPrefSize; + GetScrollbarMetrics(aState->mBoxState, mHelper.mHScrollbarBox, + nullptr, &hScrollbarPrefSize, false); + 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, true); + availISize = std::max(0, availISize - vScrollbarPrefSize.width); + } + } + + nsPresContext* presContext = PresContext(); + + // Pass false for aInit so we can pass in the correct padding. + ReflowInput + kidReflowInput(presContext, aState->mReflowInput, + mHelper.mScrolledFrame, + LogicalSize(wm, availISize, NS_UNCONSTRAINEDSIZE), + nullptr, ReflowInput::CALLER_WILL_INIT); + const nsMargin physicalPadding = padding.GetPhysicalMargin(wm); + kidReflowInput.Init(presContext, nullptr, nullptr, + &physicalPadding); + kidReflowInput.mFlags.mAssumingHScrollbar = aAssumeHScroll; + kidReflowInput.mFlags.mAssumingVScrollbar = aAssumeVScroll; + kidReflowInput.SetComputedBSize(computedBSize); + kidReflowInput.ComputedMinBSize() = computedMinBSize; + kidReflowInput.ComputedMaxBSize() = computedMaxBSize; + if (aState->mReflowInput.IsBResizeForWM(kidReflowInput.GetWritingMode())) { + kidReflowInput.SetBResize(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 NS_FRAME_NO_MOVE_FRAME isn't set) + const nsSize dummyContainerSize; + ReflowChild(mHelper.mScrolledFrame, presContext, *aMetrics, + kidReflowInput, wm, LogicalPoint(wm), dummyContainerSize, + NS_FRAME_NO_MOVE_FRAME, 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, + NS_FRAME_NO_MOVE_FRAME | NS_FRAME_NO_SIZE_VIEW); + + // XXX Some frames (e.g., nsPluginFrame, nsFrameFrame, nsTextFrame) don't bother + // setting their mOverflowArea. This is wrong because every frame should + // always set mOverflowArea. In fact nsPluginFrame and nsFrameFrame don'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(); + + if (MOZ_UNLIKELY(StyleDisplay()->mOverflowClipBox == + NS_STYLE_OVERFLOW_CLIP_BOX_CONTENT_BOX)) { + nsOverflowAreas childOverflow; + nsLayoutUtils::UnionChildOverflow(mHelper.mScrolledFrame, childOverflow); + nsRect childScrollableOverflow = childOverflow.ScrollableOverflow(); + childScrollableOverflow.Inflate(padding.GetPhysicalMargin(wm)); + nsRect contentArea = + wm.IsVertical() ? nsRect(0, 0, computedBSize, availISize) + : nsRect(0, 0, availISize, computedBSize); + if (!contentArea.Contains(childScrollableOverflow)) { + aMetrics->mOverflowAreas.ScrollableOverflow() = childScrollableOverflow; + } + } + + aState->mContentsOverflowAreas = aMetrics->mOverflowAreas; + aState->mReflowedContentsWithHScrollbar = aAssumeHScroll; + aState->mReflowedContentsWithVScrollbar = aAssumeVScroll; +} + +bool +nsHTMLScrollFrame::GuessHScrollbarNeeded(const ScrollReflowInput& aState) +{ + if (aState.mStyles.mHorizontal != NS_STYLE_OVERFLOW_AUTO) + // no guessing required + return aState.mStyles.mHorizontal == NS_STYLE_OVERFLOW_SCROLL; + + return mHelper.mHasHorizontalScrollbar; +} + +bool +nsHTMLScrollFrame::GuessVScrollbarNeeded(const ScrollReflowInput& aState) +{ + if (aState.mStyles.mVertical != NS_STYLE_OVERFLOW_AUTO) + // no guessing required + return aState.mStyles.mVertical == NS_STYLE_OVERFLOW_SCROLL; + + // 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) { + return mHelper.mHasVerticalScrollbar; + } + + // 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->GetType() == nsGkAtoms::svgOuterSVGFrame && + static_cast<nsSVGOuterSVGFrame*>(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 && (GetStateBits() & NS_FRAME_FIRST_REFLOW); +} + +void +nsHTMLScrollFrame::ReflowContents(ScrollReflowInput* aState, + const ReflowOutput& aDesiredSize) +{ + ReflowOutput kidDesiredSize(aDesiredSize.GetWritingMode(), aDesiredSize.mFlags); + ReflowScrolledFrame(aState, GuessHScrollbarNeeded(*aState), + GuessVScrollbarNeeded(*aState), &kidDesiredSize, true); + + // 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->mStyles.mVertical != NS_STYLE_OVERFLOW_SCROLL && + aState->mStyles.mHorizontal != NS_STYLE_OVERFLOW_SCROLL) { + nsSize insideBorderSize = + ComputeInsideBorderSize(aState, + nsSize(kidDesiredSize.Width(), kidDesiredSize.Height())); + 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 + ReflowScrolledFrame(aState, false, false, &kidDesiredSize, false); + } + } + + // 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. + if (TryLayout(aState, &kidDesiredSize, aState->mReflowedContentsWithHScrollbar, + aState->mReflowedContentsWithVScrollbar, false)) + return; + 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; + if (TryLayout(aState, &kidDesiredSize, false, newVScrollbarState, false)) + return; + 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. + TryLayout(aState, &kidDesiredSize, + aState->mStyles.mHorizontal != NS_STYLE_OVERFLOW_HIDDEN, + aState->mStyles.mVertical != NS_STYLE_OVERFLOW_HIDDEN, + 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()); + + nsRect scrolledArea; + // Preserve the width or height of empty rects + nsSize portSize = mHelper.mScrollPort.Size(); + nsRect scrolledRect = + mHelper.GetUnsnappedScrolledRectInternal(aState.mContentsOverflowAreas.ScrollableOverflow(), + portSize); + scrolledArea.UnionRectEdges(scrolledRect, + 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 + // -moz-hidden-unscrollable case, but scrolled frames can't have + // 'overflow' either. + // This needs to happen before SyncFrameViewAfterReflow so + // HasOverflowRect() will return the correct value. + nsOverflowAreas 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, + 0); +} + +nscoord +nsHTMLScrollFrame::GetIntrinsicVScrollbarWidth(nsRenderingContext *aRenderingContext) +{ + ScrollbarStyles ss = GetScrollbarStyles(); + if (ss.mVertical != NS_STYLE_OVERFLOW_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, true); + return vScrollbarPrefSize.width; +} + +/* virtual */ nscoord +nsHTMLScrollFrame::GetMinISize(nsRenderingContext *aRenderingContext) +{ + nscoord result = mHelper.mScrolledFrame->GetMinISize(aRenderingContext); + DISPLAY_MIN_WIDTH(this, result); + return result + GetIntrinsicVScrollbarWidth(aRenderingContext); +} + +/* virtual */ nscoord +nsHTMLScrollFrame::GetPrefISize(nsRenderingContext *aRenderingContext) +{ + nscoord result = mHelper.mScrolledFrame->GetPrefISize(aRenderingContext); + DISPLAY_PREF_WIDTH(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; +} + +// Return the <browser> if the scrollframe is for the root frame directly +// inside a <browser>. +static nsIContent* +GetBrowserRoot(nsIContent* aContent) +{ + if (aContent) { + nsIDocument* doc = aContent->GetUncomposedDoc(); + if (nsPIDOMWindowOuter* win = doc->GetWindow()) { + nsCOMPtr<Element> frameElement = win->GetFrameElementInternal(); + if (frameElement && + frameElement->NodeInfo()->Equals(nsGkAtoms::browser, kNameSpaceID_XUL)) + return frameElement; + } + } + + return nullptr; +} + +// 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. +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 (nsIFrame::ChildListIterator childLists(aCurrentFrame); + !childLists.IsDone(); childLists.Next()) { + if (skip.Contains(childLists.CurrentID())) { + continue; + } + + for (nsIFrame* child : childLists.CurrentList()) { + 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 = nsDisplayTransform::TransformRect( + child->GetScrollableOverflowRectRelativeToSelf(), child); + + // 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)); + nsRect postScroll = nsDisplayTransform::TransformRect( + child->GetScrollableOverflowRectRelativeToSelf(), child); + 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->GetScrollableOverflowRectRelativeToParent()); + } + } + } +} + +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); + + 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.mStyles.mVertical = NS_STYLE_OVERFLOW_HIDDEN; + if (!mHelper.mHScrollbarBox || mHelper.mNeverHasHorizontalScrollbar) + state.mStyles.mHorizontal = NS_STYLE_OVERFLOW_HIDDEN; + + //------------ Handle Incremental Reflow ----------------- + bool reflowHScrollbar = true; + bool reflowVScrollbar = true; + bool reflowScrollCorner = true; + if (!aReflowInput.ShouldReflowAllKids()) { + #define NEEDS_REFLOW(frame_) \ + ((frame_) && NS_SUBTREE_DIRTY(frame_)) + + 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) { + mHelper.mCollapsedResizer = true; + + nsIContent* browserRoot = GetBrowserRoot(mContent); + if (browserRoot) { + bool showResizer = browserRoot->HasAttr(kNameSpaceID_None, nsGkAtoms::showresizer); + reflowScrollCorner = showResizer == mHelper.mCollapsedResizer; + mHelper.mCollapsedResizer = !showResizer; + } + } + + nsRect oldScrollAreaBounds = mHelper.mScrollPort; + nsRect oldScrolledAreaBounds = + mHelper.mScrolledFrame->GetScrollableOverflowRectRelativeToParent(); + nsPoint oldScrollPosition = mHelper.GetScrollPosition(); + + state.mComputedBorder = aReflowInput.ComputedPhysicalBorderPadding() - + aReflowInput.ComputedPhysicalPadding(); + + ReflowContents(&state, aDesiredSize); + + aDesiredSize.Width() = state.mInsideBorderSize.width + + state.mComputedBorder.LeftRight(); + aDesiredSize.Height() = state.mInsideBorderSize.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 + PresContext()->PresShell()->PostReflowCallback(&mHelper); + mHelper.mPostedReflowCallback = true; + } + + bool didHaveHScrollbar = mHelper.mHasHorizontalScrollbar; + bool didHaveVScrollbar = mHelper.mHasVerticalScrollbar; + mHelper.mHasHorizontalScrollbar = state.mShowHScrollbar; + mHelper.mHasVerticalScrollbar = state.mShowVScrollbar; + nsRect newScrollAreaBounds = mHelper.mScrollPort; + nsRect newScrolledAreaBounds = + mHelper.mScrolledFrame->GetScrollableOverflowRectRelativeToParent(); + if (mHelper.mSkippedScrollbarLayout || + reflowHScrollbar || reflowVScrollbar || reflowScrollCorner || + (GetStateBits() & NS_FRAME_IS_DIRTY) || + didHaveHScrollbar != state.mShowHScrollbar || + didHaveVScrollbar != state.mShowVScrollbar || + !oldScrollAreaBounds.IsEqualEdges(newScrollAreaBounds) || + !oldScrolledAreaBounds.IsEqualEdges(newScrolledAreaBounds)) { + if (!mHelper.mSupppressScrollbarUpdate) { + mHelper.mSkippedScrollbarLayout = false; + mHelper.SetScrollbarVisibility(mHelper.mHScrollbarBox, state.mShowHScrollbar); + mHelper.SetScrollbarVisibility(mHelper.mVScrollbarBox, state.mShowVScrollbar); + // place and reflow scrollbars + nsRect insideBorderArea = + nsRect(nsPoint(state.mComputedBorder.left, state.mComputedBorder.top), + state.mInsideBorderSize); + mHelper.LayoutScrollbars(state.mBoxState, insideBorderArea, + oldScrollAreaBounds); + } else { + mHelper.mSkippedScrollbarLayout = true; + } + } + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + if (mHelper.IsIgnoringViewportClipping()) { + aDesiredSize.mOverflowAreas.UnionWith( + state.mContentsOverflowAreas + mHelper.mScrolledFrame->GetPosition()); + } + + 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 = NS_FRAME_COMPLETE; + NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aDesiredSize); + mHelper.PostOverflowEvent(); +} + + +//////////////////////////////////////////////////////////////////////////////// + +#ifdef DEBUG_FRAME_DUMP +nsresult +nsHTMLScrollFrame::GetFrameName(nsAString& aResult) const +{ + return MakeFrameName(NS_LITERAL_STRING("HTMLScroll"), 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() || + GetScrollbarStyles().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_TAIL_INHERITING(nsContainerFrame) + +//----------nsXULScrollFrame------------------------------------------- + +nsXULScrollFrame* +NS_NewXULScrollFrame(nsIPresShell* aPresShell, nsStyleContext* aContext, + bool aIsRoot, bool aClipAllDescendants) +{ + return new (aPresShell) nsXULScrollFrame(aContext, aIsRoot, + aClipAllDescendants); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsXULScrollFrame) + +nsXULScrollFrame::nsXULScrollFrame(nsStyleContext* aContext, + bool aIsRoot, bool aClipAllDescendants) + : nsBoxFrame(aContext, 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); + nsBox::AddMargin(mVScrollbarBox, size); + if (IsScrollbarOnRight()) + result.left = size.width; + else + result.right = size.width; + } + + if (mHScrollbarBox) { + nsSize size = mHScrollbarBox->GetXULPrefSize(*aState); + nsBox::AddMargin(mHScrollbarBox, size); + // We don't currently support any scripts that would require a scrollbar + // at the top. (Are there any?) + result.bottom = size.height; + } + + return result; +} + +nscoord +ScrollFrameHelper::GetNondisappearingScrollbarWidth(nsBoxLayoutState* aState, + WritingMode aWM) +{ + NS_ASSERTION(aState && aState->GetRenderingContext(), + "Must have rendering context in layout state for size " + "computations"); + + bool verticalWM = aWM.IsVertical(); + if (LookAndFeel::GetInt(LookAndFeel::eIntID_UseOverlayScrollbars) != 0) { + // We're using overlay scrollbars, so we need to get the width that + // non-disappearing scrollbars would have. + nsITheme* theme = aState->PresContext()->GetTheme(); + if (theme && + theme->ThemeSupportsWidget(aState->PresContext(), + verticalWM ? mHScrollbarBox + : mVScrollbarBox, + NS_THEME_SCROLLBAR_NON_DISAPPEARING)) { + LayoutDeviceIntSize size; + bool canOverride = true; + theme->GetMinimumWidgetSize(aState->PresContext(), + verticalWM ? mHScrollbarBox + : mVScrollbarBox, + NS_THEME_SCROLLBAR_NON_DISAPPEARING, + &size, + &canOverride); + return aState->PresContext()-> + DevPixelsToAppUnits(verticalWM ? size.height : size.width); + } + } + + nsMargin sizes(GetDesiredScrollbarSizes(aState)); + return verticalWM ? sizes.TopBottom() : sizes.LeftRight(); +} + +void +ScrollFrameHelper::HandleScrollbarStyleSwitching() +{ + // Check if we switched between scrollbar styles. + if (mScrollbarActivity && + LookAndFeel::GetInt(LookAndFeel::eIntID_UseOverlayScrollbars) == 0) { + mScrollbarActivity->Destroy(); + mScrollbarActivity = nullptr; + mOuter->PresContext()->ThemeChanged(); + } + else if (!mScrollbarActivity && + LookAndFeel::GetInt(LookAndFeel::eIntID_UseOverlayScrollbars) != 0) { + mScrollbarActivity = new ScrollbarActivity(do_QueryFrame(mOuter)); + mOuter->PresContext()->ThemeChanged(); + } +} + +#if defined(MOZ_B2G) || defined(MOZ_WIDGET_ANDROID) +static bool IsFocused(nsIContent* aContent) +{ + // Some content elements, like the GetContent() of a scroll frame + // for a text input field, are inside anonymous subtrees, but the focus + // manager always reports a non-anonymous element as the focused one, so + // walk up the tree until we reach a non-anonymous element. + while (aContent && aContent->IsInAnonymousSubtree()) { + aContent = aContent->GetParent(); + } + + return aContent ? nsContentUtils::IsFocusedContent(aContent) : false; +} +#endif + +void +ScrollFrameHelper::SetScrollableByAPZ(bool aScrollable) +{ + mScrollableByAPZ = aScrollable; +} + +void +ScrollFrameHelper::SetZoomableByAPZ(bool aZoomable) +{ + if (mZoomableByAPZ != aZoomable) { + // We might be changing the result of WantAsyncScroll() so schedule a + // paint to make sure we pick up the result of that change. + mZoomableByAPZ = aZoomable; + mOuter->SchedulePaint(); + } +} + +bool +ScrollFrameHelper::WantAsyncScroll() const +{ + // If zooming is allowed, and this is a frame that's allowed to zoom, then + // we want it to be async-scrollable or zooming will not be permitted. + if (mZoomableByAPZ) { + return true; + } + + ScrollbarStyles styles = GetScrollbarStylesFromFrame(); + nscoord oneDevPixel = GetScrolledFrame()->PresContext()->AppUnitsPerDevPixel(); + nsRect scrollRange = GetScrollRange(); + bool isVScrollable = (scrollRange.height >= oneDevPixel) && + (styles.mVertical != NS_STYLE_OVERFLOW_HIDDEN); + bool isHScrollable = (scrollRange.width >= oneDevPixel) && + (styles.mHorizontal != NS_STYLE_OVERFLOW_HIDDEN); + +#if defined(MOZ_B2G) || defined(MOZ_WIDGET_ANDROID) + // Mobile platforms need focus to scroll. + bool canScrollWithoutScrollbars = IsFocused(mOuter->GetContent()); +#else + bool canScrollWithoutScrollbars = true; +#endif + + // The check for scroll bars was added in bug 825692 to prevent layerization + // of text inputs for performance reasons. + bool isVAsyncScrollable = isVScrollable && (mVScrollbarBox || canScrollWithoutScrollbars); + bool isHAsyncScrollable = isHScrollable && (mHScrollbarBox || canScrollWithoutScrollbars); + return isVAsyncScrollable || isHAsyncScrollable; +} + +static nsRect +GetOnePixelRangeAroundPoint(nsPoint aPoint, bool aIsHorizontal) +{ + nsRect allowedRange(aPoint, nsSize()); + nscoord halfPixel = nsPresContext::CSSPixelsToAppUnits(0.5f); + if (aIsHorizontal) { + allowedRange.x = aPoint.x - halfPixel; + allowedRange.width = halfPixel*2 - 1; + } else { + allowedRange.y = aPoint.y - halfPixel; + allowedRange.height = halfPixel*2 - 1; + } + return allowedRange; +} + +void +ScrollFrameHelper::ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection, + nsIScrollbarMediator::ScrollSnapMode aSnap) +{ + ScrollByUnit(aScrollbar, nsIScrollableFrame::SMOOTH, aDirection, + nsIScrollableFrame::PAGES, aSnap); +} + +void +ScrollFrameHelper::ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection, + nsIScrollbarMediator::ScrollSnapMode aSnap) +{ + ScrollByUnit(aScrollbar, nsIScrollableFrame::INSTANT, aDirection, + nsIScrollableFrame::WHOLE, aSnap); +} + +void +ScrollFrameHelper::ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection, + nsIScrollbarMediator::ScrollSnapMode aSnap) +{ + bool isHorizontal = aScrollbar->IsXULHorizontal(); + nsIntPoint delta; + if (isHorizontal) { + const double kScrollMultiplier = + Preferences::GetInt("toolkit.scrollbox.horizontalScrollDistance", + NS_DEFAULT_HORIZONTAL_SCROLL_DISTANCE); + delta.x = aDirection * kScrollMultiplier; + if (GetLineScrollAmount().width * delta.x > GetPageScrollAmount().width) { + // The scroll frame is so small that the delta would be more + // than an entire page. Scroll by one page instead to maintain + // context. + ScrollByPage(aScrollbar, aDirection); + return; + } + } else { + const double kScrollMultiplier = + Preferences::GetInt("toolkit.scrollbox.verticalScrollDistance", + NS_DEFAULT_VERTICAL_SCROLL_DISTANCE); + delta.y = aDirection * kScrollMultiplier; + if (GetLineScrollAmount().height * delta.y > GetPageScrollAmount().height) { + // The scroll frame is so small that the delta would be more + // than an entire page. Scroll by one page instead to maintain + // context. + ScrollByPage(aScrollbar, aDirection); + return; + } + } + + nsIntPoint overflow; + ScrollBy(delta, nsIScrollableFrame::LINES, nsIScrollableFrame::SMOOTH, + &overflow, nsGkAtoms::other, nsIScrollableFrame::NOT_MOMENTUM, + aSnap); +} + +void +ScrollFrameHelper::RepeatButtonScroll(nsScrollbarFrame* aScrollbar) +{ + aScrollbar->MoveToNewPosition(); +} + +void +ScrollFrameHelper::ThumbMoved(nsScrollbarFrame* aScrollbar, + nscoord aOldPos, + nscoord aNewPos) +{ + MOZ_ASSERT(aScrollbar != nullptr); + bool isHorizontal = aScrollbar->IsXULHorizontal(); + nsPoint current = GetScrollPosition(); + nsPoint dest = current; + if (isHorizontal) { + dest.x = IsPhysicalLTR() ? aNewPos : aNewPos - GetScrollRange().width; + } else { + dest.y = aNewPos; + } + nsRect allowedRange = GetOnePixelRangeAroundPoint(dest, isHorizontal); + + // Don't try to scroll if we're already at an acceptable place. + // Don't call Contains here since Contains returns false when the point is + // on the bottom or right edge of the rectangle. + if (allowedRange.ClampPoint(current) == current) { + return; + } + + ScrollTo(dest, nsIScrollableFrame::INSTANT, &allowedRange); +} + +void +ScrollFrameHelper::ScrollbarReleased(nsScrollbarFrame* aScrollbar) +{ + // Scrollbar scrolling does not result in fling gestures, clear any + // accumulated velocity + mVelocityQueue.Reset(); + + // Perform scroll snapping, if needed. Scrollbar movement uses the same + // smooth scrolling animation as keyboard scrolling. + ScrollSnap(mDestination, nsIScrollableFrame::SMOOTH); +} + +void +ScrollFrameHelper::ScrollByUnit(nsScrollbarFrame* aScrollbar, + nsIScrollableFrame::ScrollMode aMode, + int32_t aDirection, + nsIScrollableFrame::ScrollUnit aUnit, + nsIScrollbarMediator::ScrollSnapMode aSnap) +{ + MOZ_ASSERT(aScrollbar != nullptr); + bool isHorizontal = aScrollbar->IsXULHorizontal(); + nsIntPoint delta; + if (isHorizontal) { + delta.x = aDirection; + } else { + delta.y = aDirection; + } + nsIntPoint overflow; + ScrollBy(delta, aUnit, aMode, &overflow, nsGkAtoms::other, + nsIScrollableFrame::NOT_MOMENTUM, aSnap); +} + +nsresult +nsXULScrollFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements) +{ + return mHelper.CreateAnonymousContent(aElements); +} + +void +nsXULScrollFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + mHelper.AppendAnonymousContentTo(aElements, aFilter); +} + +void +nsXULScrollFrame::DestroyFrom(nsIFrame* aDestructRoot) +{ + mHelper.Destroy(); + nsBoxFrame::DestroyFrom(aDestructRoot); +} + +void +nsXULScrollFrame::SetInitialChildList(ChildListID aListID, + nsFrameList& aChildList) +{ + nsBoxFrame::SetInitialChildList(aListID, aChildList); + if (aListID == kPrincipalList) { + mHelper.ReloadChildFrames(); + } +} + + +void +nsXULScrollFrame::AppendFrames(ChildListID aListID, + nsFrameList& aFrameList) +{ + nsBoxFrame::AppendFrames(aListID, aFrameList); + mHelper.ReloadChildFrames(); +} + +void +nsXULScrollFrame::InsertFrames(ChildListID aListID, + nsIFrame* aPrevFrame, + nsFrameList& aFrameList) +{ + nsBoxFrame::InsertFrames(aListID, aPrevFrame, aFrameList); + mHelper.ReloadChildFrames(); +} + +void +nsXULScrollFrame::RemoveFrame(ChildListID aListID, + nsIFrame* aOldFrame) +{ + nsBoxFrame::RemoveFrame(aListID, aOldFrame); + mHelper.ReloadChildFrames(); +} + +nsSplittableType +nsXULScrollFrame::GetSplittableType() const +{ + return NS_FRAME_NOT_SPLITTABLE; +} + +nsresult +nsXULScrollFrame::GetXULPadding(nsMargin& aMargin) +{ + aMargin.SizeTo(0,0,0,0); + return NS_OK; +} + +nsIAtom* +nsXULScrollFrame::GetType() const +{ + return nsGkAtoms::scrollFrame; +} + +nscoord +nsXULScrollFrame::GetXULBoxAscent(nsBoxLayoutState& aState) +{ + if (!mHelper.mScrolledFrame) + return 0; + + nscoord ascent = mHelper.mScrolledFrame->GetXULBoxAscent(aState); + nsMargin m(0,0,0,0); + GetXULBorderAndPadding(m); + ascent += m.top; + GetXULMargin(m); + ascent += m.top; + + return ascent; +} + +nsSize +nsXULScrollFrame::GetXULPrefSize(nsBoxLayoutState& aState) +{ +#ifdef DEBUG_LAYOUT + PropagateDebug(aState); +#endif + + nsSize pref = mHelper.mScrolledFrame->GetXULPrefSize(aState); + + ScrollbarStyles styles = GetScrollbarStyles(); + + // scrolled frames don't have their own margins + + if (mHelper.mVScrollbarBox && + styles.mVertical == NS_STYLE_OVERFLOW_SCROLL) { + nsSize vSize = mHelper.mVScrollbarBox->GetXULPrefSize(aState); + nsBox::AddMargin(mHelper.mVScrollbarBox, vSize); + pref.width += vSize.width; + } + + if (mHelper.mHScrollbarBox && + styles.mHorizontal == NS_STYLE_OVERFLOW_SCROLL) { + nsSize hSize = mHelper.mHScrollbarBox->GetXULPrefSize(aState); + nsBox::AddMargin(mHelper.mHScrollbarBox, hSize); + pref.height += hSize.height; + } + + AddBorderAndPadding(pref); + bool widthSet, heightSet; + nsIFrame::AddXULPrefSize(this, pref, widthSet, heightSet); + return pref; +} + +nsSize +nsXULScrollFrame::GetXULMinSize(nsBoxLayoutState& aState) +{ +#ifdef DEBUG_LAYOUT + PropagateDebug(aState); +#endif + + nsSize min = mHelper.mScrolledFrame->GetXULMinSizeForScrollArea(aState); + + ScrollbarStyles styles = GetScrollbarStyles(); + + if (mHelper.mVScrollbarBox && + styles.mVertical == NS_STYLE_OVERFLOW_SCROLL) { + nsSize vSize = mHelper.mVScrollbarBox->GetXULMinSize(aState); + AddMargin(mHelper.mVScrollbarBox, vSize); + min.width += vSize.width; + if (min.height < vSize.height) + min.height = vSize.height; + } + + if (mHelper.mHScrollbarBox && + styles.mHorizontal == NS_STYLE_OVERFLOW_SCROLL) { + nsSize hSize = mHelper.mHScrollbarBox->GetXULMinSize(aState); + AddMargin(mHelper.mHScrollbarBox, hSize); + min.height += hSize.height; + if (min.width < hSize.width) + min.width = hSize.width; + } + + AddBorderAndPadding(min); + bool widthSet, heightSet; + nsIFrame::AddXULMinSize(aState, this, min, widthSet, heightSet); + return min; +} + +nsSize +nsXULScrollFrame::GetXULMaxSize(nsBoxLayoutState& aState) +{ +#ifdef DEBUG_LAYOUT + PropagateDebug(aState); +#endif + + nsSize maxSize(NS_INTRINSICSIZE, NS_INTRINSICSIZE); + + AddBorderAndPadding(maxSize); + bool widthSet, heightSet; + nsIFrame::AddXULMaxSize(this, maxSize, widthSet, heightSet); + return maxSize; +} + +#ifdef DEBUG_FRAME_DUMP +nsresult +nsXULScrollFrame::GetFrameName(nsAString& aResult) const +{ + return MakeFrameName(NS_LITERAL_STRING("XULScroll"), aResult); +} +#endif + +NS_IMETHODIMP +nsXULScrollFrame::DoXULLayout(nsBoxLayoutState& aState) +{ + uint32_t flags = aState.LayoutFlags(); + nsresult rv = XULLayout(aState); + aState.SetLayoutFlags(flags); + + nsBox::DoXULLayout(aState); + return rv; +} + +NS_QUERYFRAME_HEAD(nsXULScrollFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsIScrollableFrame) + NS_QUERYFRAME_ENTRY(nsIStatefulFrame) + NS_QUERYFRAME_ENTRY(nsIScrollbarMediator) +NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame) + +//-------------------- Helper ---------------------- + +#define SMOOTH_SCROLL_PREF_NAME "general.smoothScroll" + +// AsyncSmoothMSDScroll has ref counting. +class ScrollFrameHelper::AsyncSmoothMSDScroll final : public nsARefreshObserver { +public: + AsyncSmoothMSDScroll(const nsPoint &aInitialPosition, + const nsPoint &aInitialDestination, + const nsSize &aInitialVelocity, + const nsRect &aRange, + const mozilla::TimeStamp &aStartTime, + nsPresContext* aPresContext) + : mXAxisModel(aInitialPosition.x, aInitialDestination.x, + aInitialVelocity.width, + gfxPrefs::ScrollBehaviorSpringConstant(), + gfxPrefs::ScrollBehaviorDampingRatio()) + , mYAxisModel(aInitialPosition.y, aInitialDestination.y, + aInitialVelocity.height, + gfxPrefs::ScrollBehaviorSpringConstant(), + gfxPrefs::ScrollBehaviorDampingRatio()) + , mRange(aRange) + , mLastRefreshTime(aStartTime) + , mCallee(nullptr) + , mOneDevicePixelInAppUnits(aPresContext->DevPixelsToAppUnits(1)) + { + Telemetry::SetHistogramRecordingEnabled( + Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, true); + } + + NS_INLINE_DECL_REFCOUNTING(AsyncSmoothMSDScroll, override) + + nsSize GetVelocity() { + // In nscoords per second + return nsSize(mXAxisModel.GetVelocity(), mYAxisModel.GetVelocity()); + } + + nsPoint GetPosition() { + // In nscoords + return nsPoint(NSToCoordRound(mXAxisModel.GetPosition()), NSToCoordRound(mYAxisModel.GetPosition())); + } + + void SetDestination(const nsPoint &aDestination) { + mXAxisModel.SetDestination(static_cast<int32_t>(aDestination.x)); + mYAxisModel.SetDestination(static_cast<int32_t>(aDestination.y)); + } + + void SetRange(const nsRect &aRange) + { + mRange = aRange; + } + + nsRect GetRange() + { + return mRange; + } + + void Simulate(const TimeDuration& aDeltaTime) + { + mXAxisModel.Simulate(aDeltaTime); + mYAxisModel.Simulate(aDeltaTime); + + nsPoint desired = GetPosition(); + nsPoint clamped = mRange.ClampPoint(desired); + if(desired.x != clamped.x) { + // The scroll has hit the "wall" at the left or right edge of the allowed + // scroll range. + // Absorb the impact to avoid bounceback effect. + mXAxisModel.SetVelocity(0.0); + mXAxisModel.SetPosition(clamped.x); + } + + if(desired.y != clamped.y) { + // The scroll has hit the "wall" at the left or right edge of the allowed + // scroll range. + // Absorb the impact to avoid bounceback effect. + mYAxisModel.SetVelocity(0.0); + mYAxisModel.SetPosition(clamped.y); + } + } + + bool IsFinished() + { + return mXAxisModel.IsFinished(mOneDevicePixelInAppUnits) && + mYAxisModel.IsFinished(mOneDevicePixelInAppUnits); + } + + virtual void WillRefresh(mozilla::TimeStamp aTime) override { + mozilla::TimeDuration deltaTime = aTime - mLastRefreshTime; + mLastRefreshTime = aTime; + + // The callback may release "this". + // We don't access members after returning, so no need for KungFuDeathGrip. + ScrollFrameHelper::AsyncSmoothMSDScrollCallback(mCallee, deltaTime); + } + + /* + * Set a refresh observer for smooth scroll iterations (and start observing). + * Should be used at most once during the lifetime of this object. + * Return value: true on success, false otherwise. + */ + bool SetRefreshObserver(ScrollFrameHelper *aCallee) { + NS_ASSERTION(aCallee && !mCallee, "AsyncSmoothMSDScroll::SetRefreshObserver - Invalid usage."); + + if (!RefreshDriver(aCallee)->AddRefreshObserver(this, Flush_Style)) { + return false; + } + + mCallee = aCallee; + return true; + } + +private: + // Private destructor, to discourage deletion outside of Release(): + ~AsyncSmoothMSDScroll() { + RemoveObserver(); + Telemetry::SetHistogramRecordingEnabled( + Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, false); + } + + nsRefreshDriver* RefreshDriver(ScrollFrameHelper* aCallee) { + return aCallee->mOuter->PresContext()->RefreshDriver(); + } + + /* + * The refresh driver doesn't hold a reference to its observers, + * so releasing this object can (and is) used to remove the observer on DTOR. + * Currently, this object is released once the scrolling ends. + */ + void RemoveObserver() { + if (mCallee) { + RefreshDriver(mCallee)->RemoveRefreshObserver(this, Flush_Style); + } + } + + mozilla::layers::AxisPhysicsMSDModel mXAxisModel, mYAxisModel; + nsRect mRange; + mozilla::TimeStamp mLastRefreshTime; + ScrollFrameHelper *mCallee; + nscoord mOneDevicePixelInAppUnits; +}; + +// AsyncScroll has ref counting. +class ScrollFrameHelper::AsyncScroll final + : public nsARefreshObserver, + public AsyncScrollBase +{ +public: + typedef mozilla::TimeStamp TimeStamp; + typedef mozilla::TimeDuration TimeDuration; + + explicit AsyncScroll(nsPoint aStartPos) + : AsyncScrollBase(aStartPos) + , mCallee(nullptr) + { + Telemetry::SetHistogramRecordingEnabled( + Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, true); + } + +private: + // Private destructor, to discourage deletion outside of Release(): + ~AsyncScroll() { + RemoveObserver(); + Telemetry::SetHistogramRecordingEnabled( + Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS, false); + } + +public: + void InitSmoothScroll(TimeStamp aTime, nsPoint aDestination, + nsIAtom *aOrigin, const nsRect& aRange, + const nsSize& aCurrentVelocity); + void Init(const nsRect& aRange) { + mRange = aRange; + } + + // Most recent scroll origin. + nsCOMPtr<nsIAtom> mOrigin; + + // Allowed destination positions around mDestination + nsRect mRange; + bool mIsSmoothScroll; + +private: + void InitPreferences(TimeStamp aTime, nsIAtom *aOrigin); + +// The next section is observer/callback management +// Bodies of WillRefresh and RefreshDriver contain ScrollFrameHelper specific code. +public: + NS_INLINE_DECL_REFCOUNTING(AsyncScroll, override) + + /* + * Set a refresh observer for smooth scroll iterations (and start observing). + * Should be used at most once during the lifetime of this object. + * Return value: true on success, false otherwise. + */ + bool SetRefreshObserver(ScrollFrameHelper *aCallee) { + NS_ASSERTION(aCallee && !mCallee, "AsyncScroll::SetRefreshObserver - Invalid usage."); + + if (!RefreshDriver(aCallee)->AddRefreshObserver(this, Flush_Style)) { + return false; + } + + mCallee = aCallee; + APZCCallbackHelper::SuppressDisplayport(true, mCallee->mOuter->PresContext()->PresShell()); + return true; + } + + virtual void WillRefresh(mozilla::TimeStamp aTime) override { + // The callback may release "this". + // We don't access members after returning, so no need for KungFuDeathGrip. + ScrollFrameHelper::AsyncScrollCallback(mCallee, aTime); + } + +private: + ScrollFrameHelper *mCallee; + + nsRefreshDriver* RefreshDriver(ScrollFrameHelper* aCallee) { + return aCallee->mOuter->PresContext()->RefreshDriver(); + } + + /* + * The refresh driver doesn't hold a reference to its observers, + * so releasing this object can (and is) used to remove the observer on DTOR. + * Currently, this object is released once the scrolling ends. + */ + void RemoveObserver() { + if (mCallee) { + RefreshDriver(mCallee)->RemoveRefreshObserver(this, Flush_Style); + APZCCallbackHelper::SuppressDisplayport(false, mCallee->mOuter->PresContext()->PresShell()); + } + } +}; + +/* + * Calculate duration, possibly dynamically according to events rate and event origin. + * (also maintain previous timestamps - which are only used here). + */ +void +ScrollFrameHelper::AsyncScroll::InitPreferences(TimeStamp aTime, nsIAtom *aOrigin) +{ + if (!aOrigin || aOrigin == nsGkAtoms::restore) { + // We don't have special prefs for "restore", just treat it as "other". + // "restore" scrolls are (for now) always instant anyway so unless something + // changes we should never have aOrigin == nsGkAtoms::restore here. + aOrigin = nsGkAtoms::other; + } + // Likewise we should never get APZ-triggered scrolls here, and if that changes + // something is likely broken somewhere. + MOZ_ASSERT(aOrigin != nsGkAtoms::apz); + + // Read preferences only on first iteration or for a different event origin. + if (!mIsFirstIteration && aOrigin == mOrigin) { + return; + } + + mOrigin = aOrigin; + mOriginMinMS = mOriginMaxMS = 0; + bool isOriginSmoothnessEnabled = false; + mIntervalRatio = 1; + + // Default values for all preferences are defined in all.js + static const int32_t kDefaultMinMS = 150, kDefaultMaxMS = 150; + static const bool kDefaultIsSmoothEnabled = true; + + nsAutoCString originName; + aOrigin->ToUTF8String(originName); + nsAutoCString prefBase = NS_LITERAL_CSTRING("general.smoothScroll.") + originName; + + isOriginSmoothnessEnabled = Preferences::GetBool(prefBase.get(), kDefaultIsSmoothEnabled); + if (isOriginSmoothnessEnabled) { + nsAutoCString prefMin = prefBase + NS_LITERAL_CSTRING(".durationMinMS"); + nsAutoCString prefMax = prefBase + NS_LITERAL_CSTRING(".durationMaxMS"); + mOriginMinMS = Preferences::GetInt(prefMin.get(), kDefaultMinMS); + mOriginMaxMS = Preferences::GetInt(prefMax.get(), kDefaultMaxMS); + + static const int32_t kSmoothScrollMaxAllowedAnimationDurationMS = 10000; + mOriginMaxMS = clamped(mOriginMaxMS, 0, kSmoothScrollMaxAllowedAnimationDurationMS); + mOriginMinMS = clamped(mOriginMinMS, 0, mOriginMaxMS); + } + + // Keep the animation duration longer than the average event intervals + // (to "connect" consecutive scroll animations before the scroll comes to a stop). + static const double kDefaultDurationToIntervalRatio = 2; // Duplicated at all.js + mIntervalRatio = Preferences::GetInt("general.smoothScroll.durationToIntervalRatio", + kDefaultDurationToIntervalRatio * 100) / 100.0; + + // Duration should be at least as long as the intervals -> ratio is at least 1 + mIntervalRatio = std::max(1.0, mIntervalRatio); + + if (mIsFirstIteration) { + InitializeHistory(aTime); + } +} + +void +ScrollFrameHelper::AsyncScroll::InitSmoothScroll(TimeStamp aTime, + nsPoint aDestination, + nsIAtom *aOrigin, + const nsRect& aRange, + const nsSize& aCurrentVelocity) +{ + InitPreferences(aTime, aOrigin); + mRange = aRange; + + Update(aTime, aDestination, aCurrentVelocity); +} + +bool +ScrollFrameHelper::IsSmoothScrollingEnabled() +{ + return Preferences::GetBool(SMOOTH_SCROLL_PREF_NAME, false); +} + +class ScrollFrameActivityTracker final : public nsExpirationTracker<ScrollFrameHelper,4> { +public: + // Wait for 3-4s between scrolls before we remove our layers. + // That's 4 generations of 1s each. + enum { TIMEOUT_MS = 1000 }; + ScrollFrameActivityTracker() + : nsExpirationTracker<ScrollFrameHelper,4>(TIMEOUT_MS, + "ScrollFrameActivityTracker") + {} + ~ScrollFrameActivityTracker() { + AgeAllGenerations(); + } + + virtual void NotifyExpired(ScrollFrameHelper *aObject) { + RemoveObject(aObject); + aObject->MarkNotRecentlyScrolled(); + } +}; + +static ScrollFrameActivityTracker *gScrollFrameActivityTracker = nullptr; + +// There are situations when a scroll frame is destroyed and then re-created +// for the same content element. In this case we want to increment the scroll +// generation between the old and new scrollframes. If the new one knew about +// the old one then it could steal the old generation counter and increment it +// but it doesn't have that reference so instead we use a static global to +// ensure the new one gets a fresh value. +static uint32_t sScrollGenerationCounter = 0; + +ScrollFrameHelper::ScrollFrameHelper(nsContainerFrame* aOuter, + bool aIsRoot) + : mHScrollbarBox(nullptr) + , mVScrollbarBox(nullptr) + , mScrolledFrame(nullptr) + , mScrollCornerBox(nullptr) + , mResizerBox(nullptr) + , mOuter(aOuter) + , mAsyncScroll(nullptr) + , mAsyncSmoothMSDScroll(nullptr) + , mLastScrollOrigin(nsGkAtoms::other) + , mAllowScrollOriginDowngrade(false) + , mLastSmoothScrollOrigin(nullptr) + , mScrollGeneration(++sScrollGenerationCounter) + , mDestination(0, 0) + , mScrollPosAtLastPaint(0, 0) + , mRestorePos(-1, -1) + , mLastPos(-1, -1) + , mScrollPosForLayerPixelAlignment(-1, -1) + , mLastUpdateFramesPos(-1, -1) + , mHadDisplayPortAtLastFrameUpdate(false) + , mDisplayPortAtLastFrameUpdate() + , mNeverHasVerticalScrollbar(false) + , mNeverHasHorizontalScrollbar(false) + , mHasVerticalScrollbar(false) + , mHasHorizontalScrollbar(false) + , mFrameIsUpdatingScrollbar(false) + , mDidHistoryRestore(false) + , mIsRoot(aIsRoot) + , mClipAllDescendants(aIsRoot) + , mSupppressScrollbarUpdate(false) + , mSkippedScrollbarLayout(false) + , mHadNonInitialReflow(false) + , mHorizontalOverflow(false) + , mVerticalOverflow(false) + , mPostedReflowCallback(false) + , mMayHaveDirtyFixedChildren(false) + , mUpdateScrollbarAttributes(false) + , mHasBeenScrolledRecently(false) + , mCollapsedResizer(false) + , mWillBuildScrollableLayer(false) + , mIsScrollParent(false) + , mIsScrollableLayerInRootContainer(false) + , mHasBeenScrolled(false) + , mIgnoreMomentumScroll(false) + , mTransformingByAPZ(false) + , mScrollableByAPZ(false) + , mZoomableByAPZ(false) + , mScrollsClipOnUnscrolledOutOfFlow(false) + , mVelocityQueue(aOuter->PresContext()) +{ + if (LookAndFeel::GetInt(LookAndFeel::eIntID_UseOverlayScrollbars) != 0) { + mScrollbarActivity = new ScrollbarActivity(do_QueryFrame(aOuter)); + } + + EnsureFrameVisPrefsCached(); + + if (IsAlwaysActive() && + gfxPrefs::LayersTilesEnabled() && + !nsLayoutUtils::UsesAsyncScrolling(mOuter) && + mOuter->GetContent()) { + // If we have tiling but no APZ, then set a 0-margin display port on + // active scroll containers so that we paint by whole tile increments + // when scrolling. + nsLayoutUtils::SetDisplayPortMargins(mOuter->GetContent(), + mOuter->PresContext()->PresShell(), + ScreenMargin(), + 0, + nsLayoutUtils::RepaintMode::DoNotRepaint); + nsLayoutUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors( + mOuter, nsLayoutUtils::RepaintMode::DoNotRepaint); + } + +} + +ScrollFrameHelper::~ScrollFrameHelper() +{ +} + +/* + * Callback function from AsyncSmoothMSDScroll, used in ScrollFrameHelper::ScrollTo + */ +void +ScrollFrameHelper::AsyncSmoothMSDScrollCallback(ScrollFrameHelper* aInstance, + mozilla::TimeDuration aDeltaTime) +{ + NS_ASSERTION(aInstance != nullptr, "aInstance must not be null"); + NS_ASSERTION(aInstance->mAsyncSmoothMSDScroll, + "Did not expect AsyncSmoothMSDScrollCallback without an active MSD scroll."); + + nsRect range = aInstance->mAsyncSmoothMSDScroll->GetRange(); + aInstance->mAsyncSmoothMSDScroll->Simulate(aDeltaTime); + + if (!aInstance->mAsyncSmoothMSDScroll->IsFinished()) { + nsPoint destination = aInstance->mAsyncSmoothMSDScroll->GetPosition(); + // Allow this scroll operation to land on any pixel boundary within the + // allowed scroll range for this frame. + // If the MSD is under-dampened or the destination is changed rapidly, + // it is expected (and desired) that the scrolling may overshoot. + nsRect intermediateRange = + nsRect(destination, nsSize()).UnionEdges(range); + aInstance->ScrollToImpl(destination, intermediateRange); + // 'aInstance' might be destroyed here + return; + } + + aInstance->CompleteAsyncScroll(range); +} + +/* + * Callback function from AsyncScroll, used in ScrollFrameHelper::ScrollTo + */ +void +ScrollFrameHelper::AsyncScrollCallback(ScrollFrameHelper* aInstance, + mozilla::TimeStamp aTime) +{ + MOZ_ASSERT(aInstance != nullptr, "aInstance must not be null"); + MOZ_ASSERT(aInstance->mAsyncScroll, + "Did not expect AsyncScrollCallback without an active async scroll."); + + if (!aInstance || !aInstance->mAsyncScroll) { + return; // XXX wallpaper bug 1107353 for now. + } + + nsRect range = aInstance->mAsyncScroll->mRange; + if (aInstance->mAsyncScroll->mIsSmoothScroll) { + if (!aInstance->mAsyncScroll->IsFinished(aTime)) { + nsPoint destination = aInstance->mAsyncScroll->PositionAt(aTime); + // Allow this scroll operation to land on any pixel boundary between the + // current position and the final allowed range. (We don't want + // intermediate steps to be more constrained than the final step!) + nsRect intermediateRange = + nsRect(aInstance->GetScrollPosition(), nsSize()).UnionEdges(range); + aInstance->ScrollToImpl(destination, intermediateRange); + // 'aInstance' might be destroyed here + return; + } + } + + aInstance->CompleteAsyncScroll(range); +} + +void +ScrollFrameHelper::CompleteAsyncScroll(const nsRect &aRange, nsIAtom* aOrigin) +{ + // Apply desired destination range since this is the last step of scrolling. + mAsyncSmoothMSDScroll = nullptr; + mAsyncScroll = nullptr; + nsWeakFrame weakFrame(mOuter); + ScrollToImpl(mDestination, aRange, aOrigin); + if (!weakFrame.IsAlive()) { + return; + } + // We are done scrolling, set our destination to wherever we actually ended + // up scrolling to. + mDestination = GetScrollPosition(); +} + +bool +ScrollFrameHelper::HasPluginFrames() +{ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) + if (XRE_IsContentProcess()) { + nsPresContext* presContext = mOuter->PresContext(); + nsRootPresContext* rootPresContext = presContext->GetRootPresContext(); + if (!rootPresContext || rootPresContext->NeedToComputePluginGeometryUpdates()) { + return true; + } + } +#endif + return false; +} + +bool +ScrollFrameHelper::HasPerspective() const +{ + const nsStyleDisplay* disp = mOuter->StyleDisplay(); + return disp->mChildPerspective.GetUnit() != eStyleUnit_None; +} + +bool +ScrollFrameHelper::HasBgAttachmentLocal() const +{ + const nsStyleBackground* bg = mOuter->StyleBackground(); + return bg->HasLocalBackground(); +} + +void +ScrollFrameHelper::SetScrollsClipOnUnscrolledOutOfFlow() +{ + mScrollsClipOnUnscrolledOutOfFlow = true; +} + +void +ScrollFrameHelper::ScrollToCSSPixels(const CSSIntPoint& aScrollPosition, + nsIScrollableFrame::ScrollMode aMode) +{ + nsPoint current = GetScrollPosition(); + CSSIntPoint currentCSSPixels = GetScrollPositionCSSPixels(); + nsPoint pt = CSSPoint::ToAppUnits(aScrollPosition); + nscoord halfPixel = nsPresContext::CSSPixelsToAppUnits(0.5f); + nsRect range(pt.x - halfPixel, pt.y - halfPixel, 2*halfPixel - 1, 2*halfPixel - 1); + // XXX I don't think the following blocks are needed anymore, now that + // ScrollToImpl simply tries to scroll an integer number of layer + // pixels from the current position + if (currentCSSPixels.x == aScrollPosition.x) { + pt.x = current.x; + range.x = pt.x; + range.width = 0; + } + if (currentCSSPixels.y == aScrollPosition.y) { + pt.y = current.y; + range.y = pt.y; + range.height = 0; + } + ScrollTo(pt, aMode, &range); + // 'this' might be destroyed here +} + +void +ScrollFrameHelper::ScrollToCSSPixelsApproximate(const CSSPoint& aScrollPosition, + nsIAtom *aOrigin) +{ + nsPoint pt = CSSPoint::ToAppUnits(aScrollPosition); + nscoord halfRange = nsPresContext::CSSPixelsToAppUnits(1000); + nsRect range(pt.x - halfRange, pt.y - halfRange, 2*halfRange - 1, 2*halfRange - 1); + ScrollToWithOrigin(pt, nsIScrollableFrame::INSTANT, aOrigin, &range); + // 'this' might be destroyed here +} + +CSSIntPoint +ScrollFrameHelper::GetScrollPositionCSSPixels() +{ + return CSSIntPoint::FromAppUnitsRounded(GetScrollPosition()); +} + +/* + * this method wraps calls to ScrollToImpl(), either in one shot or incrementally, + * based on the setting of the smoothness scroll pref + */ +void +ScrollFrameHelper::ScrollToWithOrigin(nsPoint aScrollPosition, + nsIScrollableFrame::ScrollMode aMode, + nsIAtom *aOrigin, + const nsRect* aRange, + nsIScrollbarMediator::ScrollSnapMode aSnap) +{ + + if (aSnap == nsIScrollableFrame::ENABLE_SNAP) { + GetSnapPointForDestination(nsIScrollableFrame::DEVICE_PIXELS, + mDestination, + aScrollPosition); + } + + nsRect scrollRange = GetScrollRangeForClamping(); + mDestination = scrollRange.ClampPoint(aScrollPosition); + if (mDestination != aScrollPosition && aOrigin == nsGkAtoms::restore && !PageIsStillLoading()) { + // If we're doing a restore but the scroll position is clamped, promote + // the origin from one that APZ can clobber to one that it can't clobber. + aOrigin = nsGkAtoms::other; + } + + nsRect range = aRange ? *aRange : nsRect(aScrollPosition, nsSize(0, 0)); + + if (aMode != nsIScrollableFrame::SMOOTH_MSD) { + // If we get a non-smooth-scroll, reset the cached APZ scroll destination, + // so that we know to process the next smooth-scroll destined for APZ. + mApzSmoothScrollDestination = Nothing(); + } + + if (aMode == nsIScrollableFrame::INSTANT) { + // Asynchronous scrolling is not allowed, so we'll kill any existing + // async-scrolling process and do an instant scroll. + CompleteAsyncScroll(range, aOrigin); + return; + } + + nsPresContext* presContext = mOuter->PresContext(); + TimeStamp now = presContext->RefreshDriver()->MostRecentRefresh(); + bool isSmoothScroll = (aMode == nsIScrollableFrame::SMOOTH) && + IsSmoothScrollingEnabled(); + + nsSize currentVelocity(0, 0); + + if (gfxPrefs::ScrollBehaviorEnabled()) { + if (aMode == nsIScrollableFrame::SMOOTH_MSD) { + mIgnoreMomentumScroll = true; + if (!mAsyncSmoothMSDScroll) { + nsPoint sv = mVelocityQueue.GetVelocity(); + currentVelocity.width = sv.x; + currentVelocity.height = sv.y; + if (mAsyncScroll) { + if (mAsyncScroll->mIsSmoothScroll) { + currentVelocity = mAsyncScroll->VelocityAt(now); + } + mAsyncScroll = nullptr; + } + + if (nsLayoutUtils::AsyncPanZoomEnabled(mOuter) && WantAsyncScroll()) { + if (mApzSmoothScrollDestination == Some(mDestination) && + mScrollGeneration == sScrollGenerationCounter) { + // If we already sent APZ a smooth-scroll request to this + // destination with this generation (i.e. it was the last request + // we sent), then don't send another one because it is redundant. + // This is to avoid a scenario where pages do repeated scrollBy + // calls, incrementing the generation counter, and blocking APZ from + // syncing the scroll offset back to the main thread. + // Note that if we get two smooth-scroll requests to the same + // destination with some other scroll in between, + // mApzSmoothScrollDestination will get reset to Nothing() and so + // we shouldn't have the problem where this check discards a + // legitimate smooth-scroll. + // Note: if there are two separate scrollframes both getting smooth + // scrolled at the same time, sScrollGenerationCounter can get + // incremented and this early-exit won't get taken. Bug 1231177 is + // on file for this. + return; + } + + // The animation will be handled in the compositor, pass the + // information needed to start the animation and skip the main-thread + // animation for this scroll. + mLastSmoothScrollOrigin = aOrigin; + mApzSmoothScrollDestination = Some(mDestination); + mScrollGeneration = ++sScrollGenerationCounter; + + if (!nsLayoutUtils::HasDisplayPort(mOuter->GetContent())) { + // If this frame doesn't have a displayport then there won't be an + // APZC instance for it and so there won't be anything to process + // this smooth scroll request. We should set a displayport on this + // frame to force an APZC which can handle the request. + nsLayoutUtils::CalculateAndSetDisplayPortMargins( + mOuter->GetScrollTargetFrame(), + nsLayoutUtils::RepaintMode::DoNotRepaint); + nsIFrame* frame = do_QueryFrame(mOuter->GetScrollTargetFrame()); + nsLayoutUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors( + frame, + nsLayoutUtils::RepaintMode::DoNotRepaint); + } + + // Schedule a paint to ensure that the frame metrics get updated on + // the compositor thread. + mOuter->SchedulePaint(); + return; + } + + mAsyncSmoothMSDScroll = + new AsyncSmoothMSDScroll(GetScrollPosition(), mDestination, + currentVelocity, GetScrollRangeForClamping(), + now, presContext); + + if (!mAsyncSmoothMSDScroll->SetRefreshObserver(this)) { + // Observer setup failed. Scroll the normal way. + CompleteAsyncScroll(range, aOrigin); + return; + } + } else { + // A previous smooth MSD scroll is still in progress, so we just need to + // update its destination. + mAsyncSmoothMSDScroll->SetDestination(mDestination); + } + + return; + } else { + if (mAsyncSmoothMSDScroll) { + currentVelocity = mAsyncSmoothMSDScroll->GetVelocity(); + mAsyncSmoothMSDScroll = nullptr; + } + } + } + + if (!mAsyncScroll) { + mAsyncScroll = new AsyncScroll(GetScrollPosition()); + if (!mAsyncScroll->SetRefreshObserver(this)) { + // Observer setup failed. Scroll the normal way. + CompleteAsyncScroll(range, aOrigin); + return; + } + } + + mAsyncScroll->mIsSmoothScroll = isSmoothScroll; + + if (isSmoothScroll) { + mAsyncScroll->InitSmoothScroll(now, mDestination, aOrigin, range, currentVelocity); + } else { + mAsyncScroll->Init(range); + } +} + +// We can't use nsContainerFrame::PositionChildViews here because +// we don't want to invalidate views that have moved. +static void AdjustViews(nsIFrame* aFrame) +{ + nsView* view = aFrame->GetView(); + if (view) { + nsPoint pt; + aFrame->GetParent()->GetClosestView(&pt); + pt += aFrame->GetPosition(); + view->SetPosition(pt.x, pt.y); + + return; + } + + if (!(aFrame->GetStateBits() & NS_FRAME_HAS_CHILD_WITH_VIEW)) { + return; + } + + // Call AdjustViews recursively for all child frames except the popup list as + // the views for popups are not scrolled. + nsIFrame::ChildListIterator lists(aFrame); + for (; !lists.IsDone(); lists.Next()) { + if (lists.CurrentID() == nsIFrame::kPopupList) { + continue; + } + nsFrameList::Enumerator childFrames(lists.CurrentList()); + for (; !childFrames.AtEnd(); childFrames.Next()) { + AdjustViews(childFrames.get()); + } + } +} + +static bool +NeedToInvalidateOnScroll(nsIFrame* aFrame) +{ + return (aFrame->GetStateBits() & NS_SCROLLFRAME_INVALIDATE_CONTENTS_ON_SCROLL) != 0; +} + +bool ScrollFrameHelper::IsIgnoringViewportClipping() const +{ + if (!mIsRoot) + return false; + nsSubDocumentFrame* subdocFrame = static_cast<nsSubDocumentFrame*> + (nsLayoutUtils::GetCrossDocParentFrame(mOuter->PresContext()->PresShell()->GetRootFrame())); + return subdocFrame && !subdocFrame->ShouldClipSubdocument(); +} + +void ScrollFrameHelper::MarkScrollbarsDirtyForReflow() const +{ + nsIPresShell* presShell = mOuter->PresContext()->PresShell(); + if (mVScrollbarBox) { + presShell->FrameNeedsReflow(mVScrollbarBox, nsIPresShell::eResize, NS_FRAME_IS_DIRTY); + } + if (mHScrollbarBox) { + presShell->FrameNeedsReflow(mHScrollbarBox, nsIPresShell::eResize, NS_FRAME_IS_DIRTY); + } +} + +bool ScrollFrameHelper::ShouldClampScrollPosition() const +{ + if (!mIsRoot) + return true; + nsSubDocumentFrame* subdocFrame = static_cast<nsSubDocumentFrame*> + (nsLayoutUtils::GetCrossDocParentFrame(mOuter->PresContext()->PresShell()->GetRootFrame())); + return !subdocFrame || subdocFrame->ShouldClampScrollPosition(); +} + +bool ScrollFrameHelper::IsAlwaysActive() const +{ + if (nsDisplayItem::ForceActiveLayers()) { + return true; + } + + // Unless this is the root scrollframe for a non-chrome document + // which is the direct child of a chrome document, we default to not + // being "active". + if (!(mIsRoot && mOuter->PresContext()->IsRootContentDocument())) { + return false; + } + + // If we have scrolled before, then we should stay active. + if (mHasBeenScrolled) { + return true; + } + + // If we're overflow:hidden, then start as inactive until + // we get scrolled manually. + ScrollbarStyles styles = GetScrollbarStylesFromFrame(); + return (styles.mHorizontal != NS_STYLE_OVERFLOW_HIDDEN && + styles.mVertical != NS_STYLE_OVERFLOW_HIDDEN); +} + +/*static*/ void +RemoveDisplayPortCallback(nsITimer* aTimer, void* aClosure) +{ + ScrollFrameHelper* helper = static_cast<ScrollFrameHelper*>(aClosure); + + // This function only ever gets called from the expiry timer, so it must + // be non-null here. Set it to null here so that we don't keep resetting + // it unnecessarily in MarkRecentlyScrolled(). + MOZ_ASSERT(helper->mDisplayPortExpiryTimer); + helper->mDisplayPortExpiryTimer = nullptr; + + if (!helper->AllowDisplayPortExpiration() || helper->mIsScrollParent) { + // If this is a scroll parent for some other scrollable frame, don't + // expire the displayport because it would break scroll handoff. Once the + // descendant scrollframes have their displayports expired, they will + // trigger the displayport expiration on this scrollframe as well, and + // mIsScrollParent will presumably be false when that kicks in. + return; + } + + // Remove the displayport from this scrollframe if it's been a while + // since it's scrolled, except if it needs to be always active. Note that + // there is one scrollframe that doesn't fall under this general rule, and + // that is the one that nsLayoutUtils::MaybeCreateDisplayPort decides to put + // a displayport on (i.e. the first scrollframe that WantAsyncScroll()s). + // If that scrollframe is this one, we remove the displayport anyway, and + // as part of the next paint MaybeCreateDisplayPort will put another + // displayport back on it. Although the displayport will "flicker" off and + // back on, the layer itself should never disappear, because this all + // happens between actual painting. If the displayport is reset to a + // different position that's ok; this scrollframe hasn't been scrolled + // recently and so the reset should be correct. + nsLayoutUtils::RemoveDisplayPort(helper->mOuter->GetContent()); + nsLayoutUtils::ExpireDisplayPortOnAsyncScrollableAncestor(helper->mOuter); + helper->mOuter->SchedulePaint(); + // Be conservative and unflag this this scrollframe as being scrollable by + // APZ. If it is still scrollable this will get flipped back soon enough. + helper->mScrollableByAPZ = false; +} + +void ScrollFrameHelper::MarkNotRecentlyScrolled() +{ + if (!mHasBeenScrolledRecently) + return; + + mHasBeenScrolledRecently = false; + mOuter->SchedulePaint(); +} + +void ScrollFrameHelper::MarkRecentlyScrolled() +{ + mHasBeenScrolledRecently = true; + if (IsAlwaysActive()) + return; + + if (mActivityExpirationState.IsTracked()) { + gScrollFrameActivityTracker->MarkUsed(this); + } else { + if (!gScrollFrameActivityTracker) { + gScrollFrameActivityTracker = new ScrollFrameActivityTracker(); + } + gScrollFrameActivityTracker->AddObject(this); + } + + // If we just scrolled and there's a displayport expiry timer in place, + // reset the timer. + ResetDisplayPortExpiryTimer(); +} + +void ScrollFrameHelper::ResetDisplayPortExpiryTimer() +{ + if (mDisplayPortExpiryTimer) { + mDisplayPortExpiryTimer->InitWithFuncCallback( + RemoveDisplayPortCallback, this, + gfxPrefs::APZDisplayPortExpiryTime(), nsITimer::TYPE_ONE_SHOT); + } +} + +bool ScrollFrameHelper::AllowDisplayPortExpiration() +{ + if (IsAlwaysActive()) { + return false; + } + if (mIsRoot && mOuter->PresContext()->IsRoot()) { + return false; + } + return true; +} + +void ScrollFrameHelper::TriggerDisplayPortExpiration() +{ + if (!AllowDisplayPortExpiration()) { + return; + } + + if (!gfxPrefs::APZDisplayPortExpiryTime()) { + // a zero time disables the expiry + return; + } + + if (!mDisplayPortExpiryTimer) { + mDisplayPortExpiryTimer = do_CreateInstance("@mozilla.org/timer;1"); + } + ResetDisplayPortExpiryTimer(); +} + +void ScrollFrameHelper::ScrollVisual() +{ + // Mark this frame as having been scrolled. If this is the root + // scroll frame of a content document, then IsAlwaysActive() + // will return true from now on and MarkNotRecentlyScrolled() won't + // have any effect. + mHasBeenScrolled = true; + + AdjustViews(mScrolledFrame); + // We need to call this after fixing up the view positions + // to be consistent with the frame hierarchy. + bool needToInvalidateOnScroll = NeedToInvalidateOnScroll(mOuter); + mOuter->RemoveStateBits(NS_SCROLLFRAME_INVALIDATE_CONTENTS_ON_SCROLL); + if (needToInvalidateOnScroll) { + MarkNotRecentlyScrolled(); + } else { + MarkRecentlyScrolled(); + } + +} + +/** + * Clamp desired scroll position aDesired and range [aDestLower, aDestUpper] + * to [aBoundLower, aBoundUpper] and then select the appunit value from among + * aBoundLower, aBoundUpper and those such that (aDesired - aCurrent) * + * aRes/aAppUnitsPerPixel is an integer (or as close as we can get + * modulo rounding to appunits) that is in [aDestLower, aDestUpper] and + * closest to aDesired. If no such value exists, return the nearest in + * [aDestLower, aDestUpper]. + */ +static nscoord +ClampAndAlignWithPixels(nscoord aDesired, + nscoord aBoundLower, nscoord aBoundUpper, + nscoord aDestLower, nscoord aDestUpper, + nscoord aAppUnitsPerPixel, double aRes, + nscoord aCurrent) +{ + // Intersect scroll range with allowed range, by clamping the ends + // of aRange to be within bounds + nscoord destLower = clamped(aDestLower, aBoundLower, aBoundUpper); + nscoord destUpper = clamped(aDestUpper, aBoundLower, aBoundUpper); + + nscoord desired = clamped(aDesired, destLower, destUpper); + + double currentLayerVal = (aRes*aCurrent)/aAppUnitsPerPixel; + double desiredLayerVal = (aRes*desired)/aAppUnitsPerPixel; + double delta = desiredLayerVal - currentLayerVal; + double nearestLayerVal = NS_round(delta) + currentLayerVal; + + // Convert back from PaintedLayer space to appunits relative to the top-left + // of the scrolled frame. + nscoord aligned = + NSToCoordRoundWithClamp(nearestLayerVal*aAppUnitsPerPixel/aRes); + + // Use a bound if it is within the allowed range and closer to desired than + // the nearest pixel-aligned value. + if (aBoundUpper == destUpper && + static_cast<decltype(Abs(desired))>(aBoundUpper - desired) < + Abs(desired - aligned)) + return aBoundUpper; + + if (aBoundLower == destLower && + static_cast<decltype(Abs(desired))>(desired - aBoundLower) < + Abs(aligned - desired)) + return aBoundLower; + + // Accept the nearest pixel-aligned value if it is within the allowed range. + if (aligned >= destLower && aligned <= destUpper) + return aligned; + + // Check if opposite pixel boundary fits into allowed range. + double oppositeLayerVal = + nearestLayerVal + ((nearestLayerVal < desiredLayerVal) ? 1.0 : -1.0); + nscoord opposite = + NSToCoordRoundWithClamp(oppositeLayerVal*aAppUnitsPerPixel/aRes); + if (opposite >= destLower && opposite <= destUpper) { + return opposite; + } + + // No alignment available. + return desired; +} + +/** + * Clamp desired scroll position aPt to aBounds and then snap + * it to the same layer pixel edges as aCurrent, keeping it within aRange + * during snapping. aCurrent is the current scroll position. + */ +static nsPoint +ClampAndAlignWithLayerPixels(const nsPoint& aPt, + const nsRect& aBounds, + const nsRect& aRange, + const nsPoint& aCurrent, + nscoord aAppUnitsPerPixel, + const gfxSize& aScale) +{ + return nsPoint(ClampAndAlignWithPixels(aPt.x, aBounds.x, aBounds.XMost(), + aRange.x, aRange.XMost(), + aAppUnitsPerPixel, aScale.width, + aCurrent.x), + ClampAndAlignWithPixels(aPt.y, aBounds.y, aBounds.YMost(), + aRange.y, aRange.YMost(), + aAppUnitsPerPixel, aScale.height, + aCurrent.y)); +} + +/* static */ void +ScrollFrameHelper::ScrollActivityCallback(nsITimer *aTimer, void* anInstance) +{ + ScrollFrameHelper* self = static_cast<ScrollFrameHelper*>(anInstance); + + // Fire the synth mouse move. + self->mScrollActivityTimer->Cancel(); + self->mScrollActivityTimer = nullptr; + self->mOuter->PresContext()->PresShell()->SynthesizeMouseMove(true); +} + + +void +ScrollFrameHelper::ScheduleSyntheticMouseMove() +{ + if (!mScrollActivityTimer) { + mScrollActivityTimer = do_CreateInstance("@mozilla.org/timer;1"); + if (!mScrollActivityTimer) + return; + } + + mScrollActivityTimer->InitWithFuncCallback( + ScrollActivityCallback, this, 100, nsITimer::TYPE_ONE_SHOT); +} + +void +ScrollFrameHelper::NotifyApproximateFrameVisibilityUpdate() +{ + mLastUpdateFramesPos = GetScrollPosition(); + mHadDisplayPortAtLastFrameUpdate = + nsLayoutUtils::GetDisplayPort(mOuter->GetContent(), + &mDisplayPortAtLastFrameUpdate); +} + +bool +ScrollFrameHelper::GetDisplayPortAtLastApproximateFrameVisibilityUpdate(nsRect* aDisplayPort) +{ + if (mHadDisplayPortAtLastFrameUpdate) { + *aDisplayPort = mDisplayPortAtLastFrameUpdate; + } + return mHadDisplayPortAtLastFrameUpdate; +} + +void +ScrollFrameHelper::ScrollToImpl(nsPoint aPt, const nsRect& aRange, nsIAtom* aOrigin) +{ + if (aOrigin == nullptr) { + // If no origin was specified, we still want to set it to something that's + // non-null, so that we can use nullness to distinguish if the frame was scrolled + // at all. Default it to some generic placeholder. + aOrigin = nsGkAtoms::other; + } + + nsPresContext* presContext = mOuter->PresContext(); + nscoord appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel(); + // 'scale' is our estimate of the scale factor that will be applied + // when rendering the scrolled content to its own PaintedLayer. + gfxSize scale = FrameLayerBuilder::GetPaintedLayerScaleForFrame(mScrolledFrame); + nsPoint curPos = GetScrollPosition(); + nsPoint alignWithPos = mScrollPosForLayerPixelAlignment == nsPoint(-1,-1) + ? curPos : mScrollPosForLayerPixelAlignment; + // Try to align aPt with curPos so they have an integer number of layer + // pixels between them. This gives us the best chance of scrolling without + // having to invalidate due to changes in subpixel rendering. + // Note that when we actually draw into a PaintedLayer, the coordinates + // that get mapped onto the layer buffer pixels are from the display list, + // which are relative to the display root frame's top-left increasing down, + // whereas here our coordinates are scroll positions which increase upward + // and are relative to the scrollport top-left. This difference doesn't actually + // matter since all we are about is that there be an integer number of + // layer pixels between pt and curPos. + nsPoint pt = + ClampAndAlignWithLayerPixels(aPt, + GetScrollRangeForClamping(), + aRange, + alignWithPos, + appUnitsPerDevPixel, + scale); + if (pt == curPos) { + return; + } + + bool needFrameVisibilityUpdate = mLastUpdateFramesPos == nsPoint(-1,-1); + + nsPoint dist(std::abs(pt.x - mLastUpdateFramesPos.x), + std::abs(pt.y - mLastUpdateFramesPos.y)); + nsSize scrollPortSize = GetScrollPositionClampingScrollPortSize(); + nscoord horzAllowance = std::max(scrollPortSize.width / std::max(sHorzScrollFraction, 1), + nsPresContext::AppUnitsPerCSSPixel()); + nscoord vertAllowance = std::max(scrollPortSize.height / std::max(sVertScrollFraction, 1), + nsPresContext::AppUnitsPerCSSPixel()); + if (dist.x >= horzAllowance || dist.y >= vertAllowance) { + needFrameVisibilityUpdate = true; + } + + // notify the listeners. + for (uint32_t i = 0; i < mListeners.Length(); i++) { + mListeners[i]->ScrollPositionWillChange(pt.x, pt.y); + } + + nsRect oldDisplayPort; + nsIContent* content = mOuter->GetContent(); + nsLayoutUtils::GetHighResolutionDisplayPort(content, &oldDisplayPort); + oldDisplayPort.MoveBy(-mScrolledFrame->GetPosition()); + + // Update frame position for scrolling + mScrolledFrame->SetPosition(mScrollPort.TopLeft() - pt); + + // If |mLastScrollOrigin| is already set to something that can clobber APZ's + // scroll offset, then we don't want to change it to something that can't. + // If we allowed this, then we could end up in a state where APZ ignores + // legitimate scroll offset updates because the origin has been masked by + // a later change within the same refresh driver tick. + bool isScrollOriginDowngrade = + nsLayoutUtils::CanScrollOriginClobberApz(mLastScrollOrigin) && + !nsLayoutUtils::CanScrollOriginClobberApz(aOrigin); + bool allowScrollOriginChange = mAllowScrollOriginDowngrade || + !isScrollOriginDowngrade; + if (allowScrollOriginChange) { + mLastScrollOrigin = aOrigin; + mAllowScrollOriginDowngrade = false; + } + mLastSmoothScrollOrigin = nullptr; + mScrollGeneration = ++sScrollGenerationCounter; + + ScrollVisual(); + + bool schedulePaint = true; + if (nsLayoutUtils::AsyncPanZoomEnabled(mOuter) && gfxPrefs::APZPaintSkipping()) { + // If APZ is enabled with paint-skipping, there are certain conditions in + // which we can skip paints: + // 1) If APZ triggered this scroll, and the tile-aligned displayport is + // unchanged. + // 2) If non-APZ triggered this scroll, but we can handle it by just asking + // APZ to update the scroll position. Again we make this conditional on + // the tile-aligned displayport being unchanged. + // We do the displayport check first since it's common to all scenarios, + // and then if the displayport is unchanged, we check if APZ triggered, + // or can handle, this scroll. If so, we set schedulePaint to false and + // skip the paint. + // Because of bug 1264297, we also don't do paint-skipping for elements with + // perspective, because the displayport may not have captured everything + // that needs to be painted. So even if the final tile-aligned displayport + // is the same, we force a repaint for these elements. Bug 1254260 tracks + // fixing this properly. + nsRect displayPort; + bool usingDisplayPort = + nsLayoutUtils::GetHighResolutionDisplayPort(content, &displayPort); + displayPort.MoveBy(-mScrolledFrame->GetPosition()); + + PAINT_SKIP_LOG("New scrollpos %s usingDP %d dpEqual %d scrollableByApz %d plugins %d perspective %d clip %d bglocal %d\n", + Stringify(CSSPoint::FromAppUnits(GetScrollPosition())).c_str(), + usingDisplayPort, displayPort.IsEqualEdges(oldDisplayPort), + mScrollableByAPZ, HasPluginFrames(), HasPerspective(), + mScrollsClipOnUnscrolledOutOfFlow, HasBgAttachmentLocal()); + if (usingDisplayPort && displayPort.IsEqualEdges(oldDisplayPort) && + !HasPerspective() && !mScrollsClipOnUnscrolledOutOfFlow && + !HasBgAttachmentLocal()) { + bool haveScrollLinkedEffects = content->GetComposedDoc()->HasScrollLinkedEffect(); + bool apzDisabled = haveScrollLinkedEffects && gfxPrefs::APZDisableForScrollLinkedEffects(); + if (!apzDisabled && !HasPluginFrames()) { + if (LastScrollOrigin() == nsGkAtoms::apz) { + schedulePaint = false; + PAINT_SKIP_LOG("Skipping due to APZ scroll\n"); + } else if (mScrollableByAPZ) { + nsIWidget* widget = presContext->GetNearestWidget(); + LayerManager* manager = widget ? widget->GetLayerManager() : nullptr; + if (manager) { + mozilla::layers::FrameMetrics::ViewID id; + DebugOnly<bool> success = nsLayoutUtils::FindIDFor(content, &id); + MOZ_ASSERT(success); // we have a displayport, we better have an ID + + // Schedule an empty transaction to carry over the scroll offset update, + // instead of a full transaction. This empty transaction might still get + // squashed into a full transaction if something happens to trigger one. + schedulePaint = false; + manager->SetPendingScrollUpdateForNextTransaction(id, + { mScrollGeneration, CSSPoint::FromAppUnits(GetScrollPosition()) }); + mOuter->SchedulePaint(nsIFrame::PAINT_COMPOSITE_ONLY); + PAINT_SKIP_LOG("Skipping due to APZ-forwarded main-thread scroll\n"); + } + } + } + } + } + + if (schedulePaint) { + mOuter->SchedulePaint(); + + if (needFrameVisibilityUpdate) { + presContext->PresShell()->ScheduleApproximateFrameVisibilityUpdateNow(); + } + } + + if (mOuter->ChildrenHavePerspective()) { + // The overflow areas of descendants may depend on the scroll position, + // so ensure they get updated. + + // First we recompute the overflow areas of the transformed children + // that use the perspective. FinishAndStoreOverflow only calls this + // if the size changes, so we need to do it manually. + mOuter->RecomputePerspectiveChildrenOverflow(mOuter); + + // Update the overflow for the scrolled frame to take any changes from the + // children into account. + mScrolledFrame->UpdateOverflow(); + + // Update the overflow for the outer so that we recompute scrollbars. + mOuter->UpdateOverflow(); + } + + ScheduleSyntheticMouseMove(); + + { // scope the AutoScrollbarRepaintSuppression + AutoScrollbarRepaintSuppression repaintSuppression(this, !schedulePaint); + nsWeakFrame weakFrame(mOuter); + UpdateScrollbarPosition(); + if (!weakFrame.IsAlive()) { + return; + } + } + + PostScrollEvent(); + + // notify the listeners. + for (uint32_t i = 0; i < mListeners.Length(); i++) { + mListeners[i]->ScrollPositionDidChange(pt.x, pt.y); + } + + nsCOMPtr<nsIDocShell> docShell = presContext->GetDocShell(); + if (docShell) { + docShell->NotifyScrollObservers(); + } +} + +static int32_t +MaxZIndexInList(nsDisplayList* aList, nsDisplayListBuilder* aBuilder) +{ + int32_t maxZIndex = -1; + for (nsDisplayItem* item = aList->GetBottom(); item; item = item->GetAbove()) { + maxZIndex = std::max(maxZIndex, item->ZIndex()); + } + return maxZIndex; +} + +// Finds the max z-index of the items in aList that meet the following conditions +// 1) have z-index auto or z-index >= 0. +// 2) aFrame is a proper ancestor of the item's frame. +// Returns -1 if there is no such item. +static int32_t +MaxZIndexInListOfItemsContainedInFrame(nsDisplayList* aList, nsIFrame* aFrame) +{ + int32_t maxZIndex = -1; + for (nsDisplayItem* item = aList->GetBottom(); item; item = item->GetAbove()) { + nsIFrame* itemFrame = item->Frame(); + // Perspective items return the scroll frame as their Frame(), so consider + // their TransformFrame() instead. + if (item->GetType() == nsDisplayItem::TYPE_PERSPECTIVE) { + itemFrame = static_cast<nsDisplayPerspective*>(item)->TransformFrame(); + } + if (nsLayoutUtils::IsProperAncestorFrame(aFrame, itemFrame)) { + maxZIndex = std::max(maxZIndex, item->ZIndex()); + } + } + return maxZIndex; +} + +template<class T> +static void +AppendInternalItemToTop(const nsDisplayListSet& aLists, + T* aItem, + int32_t aZIndex) +{ + if (aZIndex >= 0) { + aItem->SetOverrideZIndex(aZIndex); + aLists.PositionedDescendants()->AppendNewToTop(aItem); + } else { + aLists.Content()->AppendNewToTop(aItem); + } +} + +static const uint32_t APPEND_OWN_LAYER = 0x1; +static const uint32_t APPEND_POSITIONED = 0x2; +static const uint32_t APPEND_SCROLLBAR_CONTAINER = 0x4; + +static void +AppendToTop(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists, + nsDisplayList* aSource, nsIFrame* aSourceFrame, uint32_t aFlags) +{ + if (aSource->IsEmpty()) + return; + + nsDisplayWrapList* newItem; + if (aFlags & APPEND_OWN_LAYER) { + uint32_t flags = (aFlags & APPEND_SCROLLBAR_CONTAINER) + ? nsDisplayOwnLayer::SCROLLBAR_CONTAINER + : 0; + newItem = new (aBuilder) nsDisplayOwnLayer(aBuilder, aSourceFrame, aSource, flags); + } else { + newItem = new (aBuilder) nsDisplayWrapList(aBuilder, aSourceFrame, aSource); + } + + if (aFlags & APPEND_POSITIONED) { + // We want overlay scrollbars to always be on top of the scrolled content, + // but we don't want them to unnecessarily cover overlapping elements from + // outside our scroll frame. + int32_t zIndex = MaxZIndexInList(aLists.PositionedDescendants(), aBuilder); + AppendInternalItemToTop(aLists, newItem, zIndex); + } else { + aLists.BorderBackground()->AppendNewToTop(newItem); + } +} + +struct HoveredStateComparator +{ + bool Equals(nsIFrame* A, nsIFrame* B) const { + bool aHovered = A->GetContent()->HasAttr(kNameSpaceID_None, + nsGkAtoms::hover); + bool bHovered = B->GetContent()->HasAttr(kNameSpaceID_None, + nsGkAtoms::hover); + return aHovered == bHovered; + } + bool LessThan(nsIFrame* A, nsIFrame* B) const { + bool aHovered = A->GetContent()->HasAttr(kNameSpaceID_None, + nsGkAtoms::hover); + bool bHovered = B->GetContent()->HasAttr(kNameSpaceID_None, + nsGkAtoms::hover); + return !aHovered && bHovered; + } +}; + +void +ScrollFrameHelper::AppendScrollPartsTo(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists, + bool aCreateLayer, + bool aPositioned) +{ + nsITheme* theme = mOuter->PresContext()->GetTheme(); + if (theme && + theme->ShouldHideScrollbars()) { + return; + } + + bool overlayScrollbars = + LookAndFeel::GetInt(LookAndFeel::eIntID_UseOverlayScrollbars) != 0; + + AutoTArray<nsIFrame*, 3> scrollParts; + for (nsIFrame* kid : mOuter->PrincipalChildList()) { + if (kid == mScrolledFrame || + (kid->IsAbsPosContainingBlock() || overlayScrollbars) != aPositioned) + continue; + + scrollParts.AppendElement(kid); + } + if (scrollParts.IsEmpty()) { + return; + } + + // We can't check will-change budget during display list building phase. + // This means that we will build scroll bar layers for out of budget + // will-change: scroll position. + mozilla::layers::FrameMetrics::ViewID scrollTargetId = IsMaybeScrollingActive() + ? nsLayoutUtils::FindOrCreateIDFor(mScrolledFrame->GetContent()) + : mozilla::layers::FrameMetrics::NULL_SCROLL_ID; + + scrollParts.Sort(HoveredStateComparator()); + + DisplayListClipState::AutoSaveRestore clipState(aBuilder); + // Don't let scrollparts extent outside our frame's border-box, if these are + // viewport scrollbars. They would create layerization problems. This wouldn't + // normally be an issue but themes can add overflow areas to scrollbar parts. + if (mIsRoot) { + clipState.ClipContentDescendants( + mOuter->GetRectRelativeToSelf() + aBuilder->ToReferenceFrame(mOuter)); + } + + for (uint32_t i = 0; i < scrollParts.Length(); ++i) { + uint32_t flags = 0; + uint32_t appendToTopFlags = 0; + if (scrollParts[i] == mVScrollbarBox) { + flags |= nsDisplayOwnLayer::VERTICAL_SCROLLBAR; + appendToTopFlags |= APPEND_SCROLLBAR_CONTAINER; + } + if (scrollParts[i] == mHScrollbarBox) { + flags |= nsDisplayOwnLayer::HORIZONTAL_SCROLLBAR; + appendToTopFlags |= APPEND_SCROLLBAR_CONTAINER; + } + + // The display port doesn't necessarily include the scrollbars, so just + // include all of the scrollbars if we are in a RCD-RSF. We only do + // this for the root scrollframe of the root content document, which is + // zoomable, and where the scrollbar sizes are bounded by the widget. + nsRect dirty = mIsRoot && mOuter->PresContext()->IsRootContentDocument() + ? scrollParts[i]->GetVisualOverflowRectRelativeToParent() + : aDirtyRect; + nsDisplayListBuilder::AutoBuildingDisplayList + buildingForChild(aBuilder, scrollParts[i], + dirty + mOuter->GetOffsetTo(scrollParts[i]), true); + + // Always create layers for overlay scrollbars so that we don't create a + // giant layer covering the whole scrollport if both scrollbars are visible. + bool isOverlayScrollbar = (flags != 0) && overlayScrollbars; + bool createLayer = aCreateLayer || isOverlayScrollbar; + + nsDisplayListBuilder::AutoCurrentScrollbarInfoSetter + infoSetter(aBuilder, scrollTargetId, flags, createLayer); + nsDisplayListCollection partList; + mOuter->BuildDisplayListForChild( + aBuilder, scrollParts[i], dirty, partList, + nsIFrame::DISPLAY_CHILD_FORCE_STACKING_CONTEXT); + + if (createLayer) { + appendToTopFlags |= APPEND_OWN_LAYER; + } + if (aPositioned) { + appendToTopFlags |= APPEND_POSITIONED; + } + + // DISPLAY_CHILD_FORCE_STACKING_CONTEXT put everything into + // partList.PositionedDescendants(). + ::AppendToTop(aBuilder, aLists, + partList.PositionedDescendants(), scrollParts[i], + appendToTopFlags); + } +} + +/* static */ bool ScrollFrameHelper::sFrameVisPrefsCached = false; +/* static */ uint32_t ScrollFrameHelper::sHorzExpandScrollPort = 0; +/* static */ uint32_t ScrollFrameHelper::sVertExpandScrollPort = 1; +/* static */ int32_t ScrollFrameHelper::sHorzScrollFraction = 2; +/* static */ int32_t ScrollFrameHelper::sVertScrollFraction = 2; + +/* static */ void +ScrollFrameHelper::EnsureFrameVisPrefsCached() +{ + if (!sFrameVisPrefsCached) { + Preferences::AddUintVarCache(&sHorzExpandScrollPort, + "layout.framevisibility.numscrollportwidths", (uint32_t)0); + Preferences::AddUintVarCache(&sVertExpandScrollPort, + "layout.framevisibility.numscrollportheights", 1); + + Preferences::AddIntVarCache(&sHorzScrollFraction, + "layout.framevisibility.amountscrollbeforeupdatehorizontal", 2); + Preferences::AddIntVarCache(&sVertScrollFraction, + "layout.framevisibility.amountscrollbeforeupdatevertical", 2); + + sFrameVisPrefsCached = true; + } +} + +nsRect +ScrollFrameHelper::ExpandRectToNearlyVisible(const nsRect& aRect) const +{ + // We don't want to expand a rect in a direction that we can't scroll, so we + // check the scroll range. + nsRect scrollRange = GetScrollRangeForClamping(); + nsPoint scrollPos = GetScrollPosition(); + nsMargin expand(0, 0, 0, 0); + + nscoord vertShift = sVertExpandScrollPort * aRect.height; + if (scrollRange.y < scrollPos.y) { + expand.top = vertShift; + } + if (scrollPos.y < scrollRange.YMost()) { + expand.bottom = vertShift; + } + + nscoord horzShift = sHorzExpandScrollPort * aRect.width; + if (scrollRange.x < scrollPos.x) { + expand.left = horzShift; + } + if (scrollPos.x < scrollRange.XMost()) { + expand.right = horzShift; + } + + nsRect rect = aRect; + rect.Inflate(expand); + return rect; +} + +static bool +ShouldBeClippedByFrame(nsIFrame* aClipFrame, nsIFrame* aClippedFrame) +{ + return nsLayoutUtils::IsProperAncestorFrame(aClipFrame, aClippedFrame); +} + +static void +ClipItemsExceptCaret(nsDisplayList* aList, + nsDisplayListBuilder* aBuilder, + nsIFrame* aClipFrame, + const DisplayItemClip* aNonCaretClip, + const DisplayItemScrollClip* aNonCaretScrollClip) +{ + for (nsDisplayItem* i = aList->GetBottom(); i; i = i->GetAbove()) { + if (!ShouldBeClippedByFrame(aClipFrame, i->Frame())) { + continue; + } + + if (i->GetType() != nsDisplayItem::TYPE_CARET) { + bool unused; + nsRect bounds = i->GetBounds(aBuilder, &unused); + if (aNonCaretClip && aNonCaretClip->IsRectAffectedByClip(bounds)) { + DisplayItemClip newClip; + newClip.IntersectWith(i->GetClip()); + newClip.IntersectWith(*aNonCaretClip); + i->SetClip(aBuilder, newClip); + } + + if (aNonCaretScrollClip) { + const DisplayItemScrollClip* currentScrollClip = i->ScrollClip(); + MOZ_ASSERT(DisplayItemScrollClip::IsAncestor(aNonCaretScrollClip->mParent, + currentScrollClip)); + + // Overwrite the existing scroll clip with aNonCaretScrollClip, unless + // the current scroll clip is deeper than aNonCaretScrollClip (which + // means that the display item is nested inside another scroll frame). + if (!currentScrollClip || + currentScrollClip->mParent == aNonCaretScrollClip->mParent) { + i->SetScrollClip(aNonCaretScrollClip); + } + } + } + nsDisplayList* children = i->GetSameCoordinateSystemChildren(); + if (children) { + ClipItemsExceptCaret(children, aBuilder, aClipFrame, aNonCaretClip, + aNonCaretScrollClip); + } + } +} + +static void +ClipListsExceptCaret(nsDisplayListCollection* aLists, + nsDisplayListBuilder* aBuilder, + nsIFrame* aClipFrame, + const DisplayItemClip* aNonCaretClip, + const DisplayItemScrollClip* aNonCaretScrollClip) +{ + ClipItemsExceptCaret(aLists->BorderBackground(), aBuilder, aClipFrame, aNonCaretClip, aNonCaretScrollClip); + ClipItemsExceptCaret(aLists->BlockBorderBackgrounds(), aBuilder, aClipFrame, aNonCaretClip, aNonCaretScrollClip); + ClipItemsExceptCaret(aLists->Floats(), aBuilder, aClipFrame, aNonCaretClip, aNonCaretScrollClip); + ClipItemsExceptCaret(aLists->PositionedDescendants(), aBuilder, aClipFrame, aNonCaretClip, aNonCaretScrollClip); + ClipItemsExceptCaret(aLists->Outlines(), aBuilder, aClipFrame, aNonCaretClip, aNonCaretScrollClip); + ClipItemsExceptCaret(aLists->Content(), aBuilder, aClipFrame, aNonCaretClip, aNonCaretScrollClip); +} + +void +ScrollFrameHelper::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsRect& aDirtyRect, + const nsDisplayListSet& aLists) +{ + if (aBuilder->IsForFrameVisibility()) { + NotifyApproximateFrameVisibilityUpdate(); + } + + mOuter->DisplayBorderBackgroundOutline(aBuilder, aLists); + + if (aBuilder->IsPaintingToWindow()) { + mScrollPosAtLastPaint = GetScrollPosition(); + if (IsMaybeScrollingActive() && NeedToInvalidateOnScroll(mOuter)) { + MarkNotRecentlyScrolled(); + } + if (IsMaybeScrollingActive()) { + if (mScrollPosForLayerPixelAlignment == nsPoint(-1,-1)) { + mScrollPosForLayerPixelAlignment = mScrollPosAtLastPaint; + } + } else { + mScrollPosForLayerPixelAlignment = nsPoint(-1,-1); + } + } + + // It's safe to get this value before the DecideScrollableLayer call below + // because that call cannot create a displayport for root scroll frames, + // and hence it cannot create an ignore scroll frame. + bool ignoringThisScrollFrame = + aBuilder->GetIgnoreScrollFrame() == mOuter || IsIgnoringViewportClipping(); + + // Overflow clipping can never clip frames outside our subtree, so there + // is no need to worry about whether we are a moving frame that might clip + // non-moving frames. + // Not all our descendants will be clipped by overflow clipping, but all + // the ones that aren't clipped will be out of flow frames that have already + // had dirty rects saved for them by their parent frames calling + // MarkOutOfFlowChildrenForDisplayList, so it's safe to restrict our + // dirty rect here. + nsRect dirtyRect = aDirtyRect; + if (!ignoringThisScrollFrame) { + dirtyRect = dirtyRect.Intersect(mScrollPort); + } + + Unused << DecideScrollableLayer(aBuilder, &dirtyRect, + /* aAllowCreateDisplayPort = */ !mIsRoot); + + bool usingDisplayPort = aBuilder->IsPaintingToWindow() && + nsLayoutUtils::HasDisplayPort(mOuter->GetContent()); + + if (aBuilder->IsForFrameVisibility()) { + // We expand the dirty rect to catch frames just outside of the scroll port. + // We use the dirty rect instead of the whole scroll port to prevent + // too much expansion in the presence of very large (bigger than the + // viewport) scroll ports. + dirtyRect = ExpandRectToNearlyVisible(dirtyRect); + } + + // We put non-overlay scrollbars in their own layers when this is the root + // scroll frame and we are a toplevel content document. In this situation, + // the scrollbar(s) would normally be assigned their own layer anyway, since + // they're not scrolled with the rest of the document. But when both + // scrollbars are visible, the layer's visible rectangle would be the size + // of the viewport, so most layer implementations would create a layer buffer + // that's much larger than necessary. Creating independent layers for each + // scrollbar works around the problem. + bool createLayersForScrollbars = mIsRoot && + mOuter->PresContext()->IsRootContentDocument(); + + if (ignoringThisScrollFrame) { + // Root scrollframes have FrameMetrics and clipping on their container + // layers, so don't apply clipping again. + mAddClipRectToLayer = false; + + // If we are a root scroll frame that has a display port we want to add + // scrollbars, they will be children of the scrollable layer, but they get + // adjusted by the APZC automatically. + bool addScrollBars = mIsRoot && usingDisplayPort && !aBuilder->IsForEventDelivery(); + + if (addScrollBars) { + // Add classic scrollbars. + AppendScrollPartsTo(aBuilder, aDirtyRect, aLists, + createLayersForScrollbars, false); + } + + // Don't clip the scrolled child, and don't paint scrollbars/scrollcorner. + // The scrolled frame shouldn't have its own background/border, so we + // can just pass aLists directly. + mOuter->BuildDisplayListForChild(aBuilder, mScrolledFrame, + dirtyRect, aLists); + + if (addScrollBars) { + // Add overlay scrollbars. + AppendScrollPartsTo(aBuilder, aDirtyRect, aLists, + createLayersForScrollbars, true); + } + + return; + } + + // Root scrollframes have FrameMetrics and clipping on their container + // layers, so don't apply clipping again. + mAddClipRectToLayer = + !(mIsRoot && mOuter->PresContext()->PresShell()->GetIsViewportOverridden()); + + // Whether we might want to build a scrollable layer for this scroll frame + // at some point in the future. This controls whether we add the information + // to the layer tree (a scroll info layer if necessary, and add the right + // area to the dispatch to content layer event regions) necessary to activate + // a scroll frame so it creates a scrollable layer. + bool couldBuildLayer = false; + if (mWillBuildScrollableLayer) { + couldBuildLayer = true; + } else { + couldBuildLayer = + nsLayoutUtils::AsyncPanZoomEnabled(mOuter) && + WantAsyncScroll() && + // If we are using containers for root frames, and we are the root + // scroll frame for the display root, then we don't need a scroll + // info layer. nsDisplayList::PaintForFrame already calls + // ComputeFrameMetrics for us. + (!(gfxPrefs::LayoutUseContainersForRootFrames() && mIsRoot) || + (aBuilder->RootReferenceFrame()->PresContext() != mOuter->PresContext())); + } + + // Now display the scrollbars and scrollcorner. These parts are drawn + // in the border-background layer, on top of our own background and + // borders and underneath borders and backgrounds of later elements + // in the tree. + // Note that this does not apply for overlay scrollbars; those are drawn + // in the positioned-elements layer on top of everything else by the call + // to AppendScrollPartsTo(..., true) further down. + AppendScrollPartsTo(aBuilder, aDirtyRect, aLists, + createLayersForScrollbars, false); + + const nsStyleDisplay* disp = mOuter->StyleDisplay(); + if (disp && (disp->mWillChangeBitField & NS_STYLE_WILL_CHANGE_SCROLL)) { + aBuilder->AddToWillChangeBudget(mOuter, GetScrollPositionClampingScrollPortSize()); + } + + mScrollParentID = aBuilder->GetCurrentScrollParentId(); + + Maybe<nsRect> contentBoxClipForCaret; + Maybe<nsRect> contentBoxClipForNonCaretContent; + if (MOZ_UNLIKELY(mOuter->StyleDisplay()->mOverflowClipBox == + NS_STYLE_OVERFLOW_CLIP_BOX_CONTENT_BOX)) { + // We only clip if there is *scrollable* overflow, to avoid clipping + // *visual* overflow unnecessarily. + nsRect clipRect = mScrollPort + aBuilder->ToReferenceFrame(mOuter); + nsRect so = mScrolledFrame->GetScrollableOverflowRect(); + if (clipRect.width != so.width || clipRect.height != so.height || + so.x < 0 || so.y < 0) { + clipRect.Deflate(mOuter->GetUsedPadding()); + contentBoxClipForNonCaretContent = Some(clipRect); + if (nsIFrame* caretFrame = aBuilder->GetCaretFrame()) { + // Avoid clipping it in a zero-height line box (heuristic only). + if (caretFrame->GetRect().height != 0) { + nsRect caretRect = aBuilder->GetCaretRect(); + // Allow the caret to stick out of the content box clip by half the + // caret height on the top, and its full width on the right. + clipRect.Inflate(nsMargin(caretRect.height / 2, caretRect.width, 0, 0)); + contentBoxClipForCaret = Some(clipRect); + } + } + } + } + + nsIScrollableFrame* sf = do_QueryFrame(mOuter); + MOZ_ASSERT(sf); + + nsDisplayListCollection scrolledContent; + { + // Note that setting the current scroll parent id here means that positioned children + // of this scroll info layer will pick up the scroll info layer as their scroll handoff + // parent. This is intentional because that is what happens for positioned children + // of scroll layers, and we want to maintain consistent behaviour between scroll layers + // and scroll info layers. + nsDisplayListBuilder::AutoCurrentScrollParentIdSetter idSetter( + aBuilder, + couldBuildLayer && mScrolledFrame->GetContent() + ? nsLayoutUtils::FindOrCreateIDFor(mScrolledFrame->GetContent()) + : aBuilder->GetCurrentScrollParentId()); + + nsRect clipRect = mScrollPort + aBuilder->ToReferenceFrame(mOuter); + // Our override of GetBorderRadii ensures we never have a radius at + // the corners where we have a scrollbar. + nscoord radii[8]; + bool haveRadii = mOuter->GetPaddingBoxBorderRadii(radii); + if (mIsRoot) { + clipRect.SizeTo(nsLayoutUtils::CalculateCompositionSizeForFrame(mOuter)); + if (mOuter->PresContext()->IsRootContentDocument()) { + double res = mOuter->PresContext()->PresShell()->GetResolution(); + clipRect.width = NSToCoordRound(clipRect.width / res); + clipRect.height = NSToCoordRound(clipRect.height / res); + } + } + + DisplayListClipState::AutoSaveRestore clipState(aBuilder); + if (mClipAllDescendants) { + clipState.ClipContentDescendants(clipRect, haveRadii ? radii : nullptr); + } else { + clipState.ClipContainingBlockDescendants(clipRect, haveRadii ? radii : nullptr); + } + + DisplayItemScrollClip* inactiveScrollClip = nullptr; + + { + DisplayListClipState::AutoSaveRestore contentBoxClipState(aBuilder); + if (contentBoxClipForCaret) { + if (mClipAllDescendants) { + contentBoxClipState.ClipContentDescendants(*contentBoxClipForCaret); + } else { + contentBoxClipState.ClipContainingBlockDescendants(*contentBoxClipForCaret); + } + } + + DisplayListClipState::AutoSaveRestore clipStateForScrollClip(aBuilder); + if (mWillBuildScrollableLayer) { + if (mClipAllDescendants) { + clipStateForScrollClip.TurnClipIntoScrollClipForContentDescendants(aBuilder, sf); + } else { + clipStateForScrollClip.TurnClipIntoScrollClipForContainingBlockDescendants(aBuilder, sf); + } + } else { + // Create a scroll clip anyway because we might need to activate for scroll handoff. + if (mClipAllDescendants) { + inactiveScrollClip = clipStateForScrollClip.InsertInactiveScrollClipForContentDescendants(aBuilder, sf); + } else { + inactiveScrollClip = clipStateForScrollClip.InsertInactiveScrollClipForContainingBlockDescendants(aBuilder, sf); + } + MOZ_ASSERT(!inactiveScrollClip->mIsAsyncScrollable); + } + + // Clip our contents to the unsnapped scrolled rect. This makes sure that + // we don't have display items over the subpixel seam at the edge of the + // scrolled area. + DisplayListClipState::AutoSaveRestore scrolledRectClipState(aBuilder); + nsRect scrolledRectClip = + GetUnsnappedScrolledRectInternal(mScrolledFrame->GetScrollableOverflowRect(), + mScrollPort.Size()) + mScrolledFrame->GetPosition(); + if (usingDisplayPort) { + // Clip the contents to the display port. + // The dirty rect already acts kind of like a clip, in that + // FrameLayerBuilder intersects item bounds and opaque regions with + // it, but it doesn't have the consistent snapping behavior of a + // true clip. + // For a case where this makes a difference, imagine the following + // scenario: The display port has an edge that falls on a fractional + // layer pixel, and there's an opaque display item that covers the + // whole display port up until that fractional edge, and there is a + // transparent display item that overlaps the edge. We want to prevent + // this transparent item from enlarging the scrolled layer's visible + // region beyond its opaque region. The dirty rect doesn't do that - + // it gets rounded out, whereas a true clip gets rounded to nearest + // pixels. + // If there is no display port, we don't need this because the clip + // from the scroll port is still applied. + scrolledRectClip = scrolledRectClip.Intersect(dirtyRect); + } + scrolledRectClipState.ClipContainingBlockDescendants( + scrolledRectClip + aBuilder->ToReferenceFrame(mOuter)); + + mOuter->BuildDisplayListForChild(aBuilder, mScrolledFrame, dirtyRect, scrolledContent); + } + + if (contentBoxClipForNonCaretContent) { + DisplayListClipState::AutoSaveRestore contentBoxClipState(aBuilder); + if (mClipAllDescendants) { + contentBoxClipState.ClipContentDescendants(*contentBoxClipForNonCaretContent); + } else { + contentBoxClipState.ClipContainingBlockDescendants(*contentBoxClipForNonCaretContent); + } + + DisplayListClipState::AutoSaveRestore clipStateForScrollClip(aBuilder); + if (mWillBuildScrollableLayer) { + if (mClipAllDescendants) { + clipStateForScrollClip.TurnClipIntoScrollClipForContentDescendants(aBuilder, sf); + } else { + clipStateForScrollClip.TurnClipIntoScrollClipForContainingBlockDescendants(aBuilder, sf); + } + } + const DisplayItemClip* clipNonCaret = aBuilder->ClipState().GetCurrentCombinedClip(aBuilder); + const DisplayItemScrollClip* scrollClipNonCaret = aBuilder->ClipState().GetCurrentInnermostScrollClip(); + ClipListsExceptCaret(&scrolledContent, aBuilder, mScrolledFrame, + clipNonCaret, scrollClipNonCaret); + } + + if (aBuilder->IsPaintingToWindow()) { + mIsScrollParent = idSetter.ShouldForceLayerForScrollParent(); + } + if (idSetter.ShouldForceLayerForScrollParent() && + !gfxPrefs::LayoutUseContainersForRootFrames()) + { + // Note that forcing layerization of scroll parents follows the scroll + // handoff chain which is subject to the out-of-flow-frames caveat noted + // above (where the idSetter variable is created). + // + // This is not compatible when using containes for root scrollframes. + MOZ_ASSERT(couldBuildLayer && mScrolledFrame->GetContent()); + if (inactiveScrollClip) { + inactiveScrollClip->mIsAsyncScrollable = true; + } + if (!mWillBuildScrollableLayer) { + // Set a displayport so next paint we don't have to force layerization + // after the fact. + nsLayoutUtils::SetDisplayPortMargins(mOuter->GetContent(), + mOuter->PresContext()->PresShell(), + ScreenMargin(), + 0, + nsLayoutUtils::RepaintMode::DoNotRepaint); + // Call DecideScrollableLayer to recompute mWillBuildScrollableLayer and + // recompute the current animated geometry root if needed. + // It's too late to change the dirty rect so pass a copy. + nsRect copyOfDirtyRect = dirtyRect; + Unused << DecideScrollableLayer(aBuilder, ©OfDirtyRect, + /* aAllowCreateDisplayPort = */ false); + } + } + } + + if (mWillBuildScrollableLayer) { + aBuilder->ForceLayerForScrollParent(); + } + + if (couldBuildLayer) { + // Make sure that APZ will dispatch events back to content so we can create + // a displayport for this frame. We'll add the item later on. + nsDisplayLayerEventRegions* inactiveRegionItem = nullptr; + if (aBuilder->IsPaintingToWindow() && + !mWillBuildScrollableLayer && + aBuilder->IsBuildingLayerEventRegions()) + { + inactiveRegionItem = new (aBuilder) nsDisplayLayerEventRegions(aBuilder, mScrolledFrame); + inactiveRegionItem->AddInactiveScrollPort(mScrollPort + aBuilder->ToReferenceFrame(mOuter)); + } + + if (inactiveRegionItem) { + int32_t zIndex = + MaxZIndexInListOfItemsContainedInFrame(scrolledContent.PositionedDescendants(), mOuter); + AppendInternalItemToTop(scrolledContent, inactiveRegionItem, zIndex); + } + + if (aBuilder->ShouldBuildScrollInfoItemsForHoisting()) { + aBuilder->AppendNewScrollInfoItemForHoisting( + new (aBuilder) nsDisplayScrollInfoLayer(aBuilder, mScrolledFrame, + mOuter)); + } + } + // Now display overlay scrollbars and the resizer, if we have one. + AppendScrollPartsTo(aBuilder, aDirtyRect, scrolledContent, + createLayersForScrollbars, true); + scrolledContent.MoveTo(aLists); +} + +bool +ScrollFrameHelper::DecideScrollableLayer(nsDisplayListBuilder* aBuilder, + nsRect* aDirtyRect, + bool aAllowCreateDisplayPort) +{ + // Save and check if this changes so we can recompute the current agr. + bool oldWillBuildScrollableLayer = mWillBuildScrollableLayer; + + bool wasUsingDisplayPort = false; + bool usingDisplayPort = false; + nsIContent* content = mOuter->GetContent(); + if (aBuilder->IsPaintingToWindow()) { + wasUsingDisplayPort = nsLayoutUtils::HasDisplayPort(content); + + if (aAllowCreateDisplayPort) { + nsLayoutUtils::MaybeCreateDisplayPort(*aBuilder, mOuter); + + nsRect displayportBase = *aDirtyRect; + nsPresContext* pc = mOuter->PresContext(); + if (mIsRoot && (pc->IsRootContentDocument() || !pc->GetParentPresContext())) { + displayportBase = + nsRect(nsPoint(0, 0), nsLayoutUtils::CalculateCompositionSizeForFrame(mOuter)); + } else { + // Make the displayport base equal to the dirty rect restricted to + // the scrollport and the root composition bounds, relative to the + // scrollport. + displayportBase = aDirtyRect->Intersect(mScrollPort); + + // Only restrict to the root composition bounds if necessary, + // as the required coordinate transformation is expensive. + if (wasUsingDisplayPort) { + const nsPresContext* rootPresContext = + pc->GetToplevelContentDocumentPresContext(); + if (!rootPresContext) { + rootPresContext = pc->GetRootPresContext(); + } + if (rootPresContext) { + const nsIPresShell* const rootPresShell = rootPresContext->PresShell(); + nsIFrame* rootFrame = rootPresShell->GetRootScrollFrame(); + if (!rootFrame) { + rootFrame = rootPresShell->GetRootFrame(); + } + if (rootFrame) { + nsRect rootCompBounds = + nsRect(nsPoint(0, 0), nsLayoutUtils::CalculateCompositionSizeForFrame(rootFrame)); + + // If rootFrame is the RCD-RSF then CalculateCompositionSizeForFrame + // did not take the document's resolution into account, so we must. + if (rootPresContext->IsRootContentDocument() && + rootFrame == rootPresShell->GetRootScrollFrame()) { + rootCompBounds = rootCompBounds.RemoveResolution(rootPresShell->GetResolution()); + } + + // We want to convert the root composition bounds from the coordinate + // space of |rootFrame| to the coordinate space of |mOuter|. We do + // that with the TransformRect call below. However, since we care + // about the root composition bounds relative to what the user is + // actually seeing, we also need to incorporate the APZ callback + // transforms into this. Most of the time those transforms are + // negligible, but in some cases (e.g. when a zoom is applied on + // an overflow:hidden document) it is not (see bug 1280013). + // XXX: Eventually we may want to create a modified version of + // TransformRect that includes the APZ callback transforms + // directly. + nsLayoutUtils::TransformRect(rootFrame, mOuter, rootCompBounds); + rootCompBounds += CSSPoint::ToAppUnits( + nsLayoutUtils::GetCumulativeApzCallbackTransform(mOuter)); + + displayportBase = displayportBase.Intersect(rootCompBounds); + } + } + } + + displayportBase -= mScrollPort.TopLeft(); + } + + nsLayoutUtils::SetDisplayPortBase(mOuter->GetContent(), displayportBase); + } + + // If we don't have aAllowCreateDisplayPort == true then should have already + // been called with aAllowCreateDisplayPort == true which should have set a + // displayport base. + MOZ_ASSERT(content->GetProperty(nsGkAtoms::DisplayPortBase)); + nsRect displayPort; + usingDisplayPort = + nsLayoutUtils::GetDisplayPort(content, &displayPort, RelativeTo::ScrollFrame); + + if (usingDisplayPort) { + // Override the dirty rectangle if the displayport has been set. + *aDirtyRect = displayPort; + } else if (mIsRoot) { + // The displayPort getter takes care of adjusting for resolution. So if + // we have resolution but no displayPort then we need to adjust for + // resolution here. + nsIPresShell* presShell = mOuter->PresContext()->PresShell(); + *aDirtyRect = aDirtyRect->RemoveResolution( + presShell->ScaleToResolution() ? presShell->GetResolution () : 1.0f); + } + } + + // Since making new layers is expensive, only create a scrollable layer + // for some scroll frames. + // When a displayport is being used, force building of a layer so that + // the compositor can find the scrollable layer for async scrolling. + // If the element is marked 'scrollgrab', also force building of a layer + // so that APZ can implement scroll grabbing. + mWillBuildScrollableLayer = usingDisplayPort || nsContentUtils::HasScrollgrab(content); + + // The cached animated geometry root for the display builder is out of + // date if we just introduced a new animated geometry root. + if ((oldWillBuildScrollableLayer != mWillBuildScrollableLayer) || (wasUsingDisplayPort != usingDisplayPort)) { + aBuilder->RecomputeCurrentAnimatedGeometryRoot(); + } + + if (gfxPrefs::LayoutUseContainersForRootFrames() && mWillBuildScrollableLayer && mIsRoot) { + mIsScrollableLayerInRootContainer = true; + } + + return mWillBuildScrollableLayer; +} + + +Maybe<ScrollMetadata> +ScrollFrameHelper::ComputeScrollMetadata(Layer* aLayer, + nsIFrame* aContainerReferenceFrame, + const ContainerLayerParameters& aParameters, + const DisplayItemClip* aClip) const +{ + if (!mWillBuildScrollableLayer || mIsScrollableLayerInRootContainer) { + return Nothing(); + } + + nsPoint toReferenceFrame = mOuter->GetOffsetToCrossDoc(aContainerReferenceFrame); + + Maybe<nsRect> parentLayerClip; + // For containerful frames, the clip is on the container layer. + if (aClip && + (!gfxPrefs::LayoutUseContainersForRootFrames() || mAddClipRectToLayer)) { + parentLayerClip = Some(aClip->GetClipRect()); + } + + bool isRootContent = mIsRoot && mOuter->PresContext()->IsRootContentDocument(); + bool thisScrollFrameUsesAsyncScrolling = nsLayoutUtils::UsesAsyncScrolling(mOuter); + if (!thisScrollFrameUsesAsyncScrolling) { + if (parentLayerClip) { + // If APZ is not enabled, we still need the displayport to be clipped + // in the compositor. + ParentLayerIntRect displayportClip = + ViewAs<ParentLayerPixel>( + parentLayerClip->ScaleToNearestPixels( + aParameters.mXScale, + aParameters.mYScale, + mScrolledFrame->PresContext()->AppUnitsPerDevPixel())); + + ParentLayerIntRect layerClip; + if (const ParentLayerIntRect* origClip = aLayer->GetClipRect().ptrOr(nullptr)) { + layerClip = displayportClip.Intersect(*origClip); + } else { + layerClip = displayportClip; + } + aLayer->SetClipRect(Some(layerClip)); + } + + // Return early, since if we don't use APZ we don't need FrameMetrics. + return Nothing(); + } + + MOZ_ASSERT(mScrolledFrame->GetContent()); + + nsRect scrollport = mScrollPort + toReferenceFrame; + + return Some(nsLayoutUtils::ComputeScrollMetadata( + mScrolledFrame, mOuter, mOuter->GetContent(), + aContainerReferenceFrame, aLayer, mScrollParentID, + scrollport, parentLayerClip, isRootContent, aParameters)); +} + +bool +ScrollFrameHelper::IsRectNearlyVisible(const nsRect& aRect) const +{ + // Use the right rect depending on if a display port is set. + nsRect displayPort; + bool usingDisplayport = + nsLayoutUtils::GetDisplayPort(mOuter->GetContent(), &displayPort, RelativeTo::ScrollFrame); + return aRect.Intersects(ExpandRectToNearlyVisible(usingDisplayport ? displayPort : mScrollPort)); +} + +static void HandleScrollPref(nsIScrollable *aScrollable, int32_t aOrientation, + uint8_t& aValue) +{ + int32_t pref; + aScrollable->GetDefaultScrollbarPreferences(aOrientation, &pref); + switch (pref) { + case nsIScrollable::Scrollbar_Auto: + // leave |aValue| untouched + break; + case nsIScrollable::Scrollbar_Never: + aValue = NS_STYLE_OVERFLOW_HIDDEN; + break; + case nsIScrollable::Scrollbar_Always: + aValue = NS_STYLE_OVERFLOW_SCROLL; + break; + } +} + +ScrollbarStyles +ScrollFrameHelper::GetScrollbarStylesFromFrame() const +{ + nsPresContext* presContext = mOuter->PresContext(); + if (!presContext->IsDynamic() && + !(mIsRoot && presContext->HasPaginatedScrolling())) { + return ScrollbarStyles(NS_STYLE_OVERFLOW_HIDDEN, NS_STYLE_OVERFLOW_HIDDEN); + } + + if (!mIsRoot) { + const nsStyleDisplay* disp = mOuter->StyleDisplay(); + return ScrollbarStyles(disp); + } + + ScrollbarStyles result = presContext->GetViewportScrollbarStylesOverride(); + nsCOMPtr<nsISupports> container = presContext->GetContainerWeak(); + nsCOMPtr<nsIScrollable> scrollable = do_QueryInterface(container); + if (scrollable) { + HandleScrollPref(scrollable, nsIScrollable::ScrollOrientation_X, + result.mHorizontal); + HandleScrollPref(scrollable, nsIScrollable::ScrollOrientation_Y, + result.mVertical); + } + return result; +} + +nsRect +ScrollFrameHelper::GetScrollRange() const +{ + return GetScrollRange(mScrollPort.width, mScrollPort.height); +} + +nsRect +ScrollFrameHelper::GetScrollRange(nscoord aWidth, nscoord aHeight) const +{ + nsRect range = GetScrolledRect(); + range.width = std::max(range.width - aWidth, 0); + range.height = std::max(range.height - aHeight, 0); + return range; +} + +nsRect +ScrollFrameHelper::GetScrollRangeForClamping() const +{ + if (!ShouldClampScrollPosition()) { + return nsRect(nscoord_MIN/2, nscoord_MIN/2, + nscoord_MAX - nscoord_MIN/2, nscoord_MAX - nscoord_MIN/2); + } + nsSize scrollPortSize = GetScrollPositionClampingScrollPortSize(); + return GetScrollRange(scrollPortSize.width, scrollPortSize.height); +} + +nsSize +ScrollFrameHelper::GetScrollPositionClampingScrollPortSize() const +{ + nsIPresShell* presShell = mOuter->PresContext()->PresShell(); + if (mIsRoot && presShell->IsScrollPositionClampingScrollPortSizeSet()) { + return presShell->GetScrollPositionClampingScrollPortSize(); + } + return mScrollPort.Size(); +} + +static void +AdjustForWholeDelta(int32_t aDelta, nscoord* aCoord) +{ + if (aDelta < 0) { + *aCoord = nscoord_MIN; + } else if (aDelta > 0) { + *aCoord = nscoord_MAX; + } +} + +/** + * Calculate lower/upper scrollBy range in given direction. + * @param aDelta specifies scrollBy direction, if 0 then range will be 0 size + * @param aPos desired destination in AppUnits + * @param aNeg/PosTolerance defines relative range distance + * below and above of aPos point + * @param aMultiplier used for conversion of tolerance into appUnis + */ +static void +CalcRangeForScrollBy(int32_t aDelta, nscoord aPos, + float aNegTolerance, + float aPosTolerance, + nscoord aMultiplier, + nscoord* aLower, nscoord* aUpper) +{ + if (!aDelta) { + *aLower = *aUpper = aPos; + return; + } + *aLower = aPos - NSToCoordRound(aMultiplier * (aDelta > 0 ? aNegTolerance : aPosTolerance)); + *aUpper = aPos + NSToCoordRound(aMultiplier * (aDelta > 0 ? aPosTolerance : aNegTolerance)); +} + +void +ScrollFrameHelper::ScrollBy(nsIntPoint aDelta, + nsIScrollableFrame::ScrollUnit aUnit, + nsIScrollableFrame::ScrollMode aMode, + nsIntPoint* aOverflow, + nsIAtom *aOrigin, + nsIScrollableFrame::ScrollMomentum aMomentum, + nsIScrollbarMediator::ScrollSnapMode aSnap) +{ + // When a smooth scroll is being processed on a frame, mouse wheel and trackpad + // momentum scroll event updates must notcancel the SMOOTH or SMOOTH_MSD + // scroll animations, enabling Javascript that depends on them to be responsive + // without forcing the user to wait for the fling animations to completely stop. + switch (aMomentum) { + case nsIScrollableFrame::NOT_MOMENTUM: + mIgnoreMomentumScroll = false; + break; + case nsIScrollableFrame::SYNTHESIZED_MOMENTUM_EVENT: + if (mIgnoreMomentumScroll) { + return; + } + break; + } + + if (mAsyncSmoothMSDScroll != nullptr) { + // When CSSOM-View scroll-behavior smooth scrolling is interrupted, + // the scroll is not completed to avoid non-smooth snapping to the + // prior smooth scroll's destination. + mDestination = GetScrollPosition(); + } + + nsSize deltaMultiplier; + float negativeTolerance; + float positiveTolerance; + if (!aOrigin){ + aOrigin = nsGkAtoms::other; + } + bool isGenericOrigin = (aOrigin == nsGkAtoms::other); + switch (aUnit) { + case nsIScrollableFrame::DEVICE_PIXELS: { + nscoord appUnitsPerDevPixel = + mOuter->PresContext()->AppUnitsPerDevPixel(); + deltaMultiplier = nsSize(appUnitsPerDevPixel, appUnitsPerDevPixel); + if (isGenericOrigin){ + aOrigin = nsGkAtoms::pixels; + } + negativeTolerance = positiveTolerance = 0.5f; + break; + } + case nsIScrollableFrame::LINES: { + deltaMultiplier = GetLineScrollAmount(); + if (isGenericOrigin){ + aOrigin = nsGkAtoms::lines; + } + negativeTolerance = positiveTolerance = 0.1f; + break; + } + case nsIScrollableFrame::PAGES: { + deltaMultiplier = GetPageScrollAmount(); + if (isGenericOrigin){ + aOrigin = nsGkAtoms::pages; + } + negativeTolerance = 0.05f; + positiveTolerance = 0; + break; + } + case nsIScrollableFrame::WHOLE: { + nsPoint pos = GetScrollPosition(); + AdjustForWholeDelta(aDelta.x, &pos.x); + AdjustForWholeDelta(aDelta.y, &pos.y); + if (aSnap == nsIScrollableFrame::ENABLE_SNAP) { + GetSnapPointForDestination(aUnit, mDestination, pos); + } + ScrollTo(pos, aMode); + // 'this' might be destroyed here + if (aOverflow) { + *aOverflow = nsIntPoint(0, 0); + } + return; + } + default: + NS_ERROR("Invalid scroll mode"); + return; + } + + nsPoint newPos = mDestination + nsPoint(aDelta.x*deltaMultiplier.width, aDelta.y*deltaMultiplier.height); + + if (aSnap == nsIScrollableFrame::ENABLE_SNAP) { + ScrollbarStyles styles = GetScrollbarStylesFromFrame(); + if (styles.mScrollSnapTypeY != NS_STYLE_SCROLL_SNAP_TYPE_NONE || + styles.mScrollSnapTypeX != NS_STYLE_SCROLL_SNAP_TYPE_NONE) { + nscoord appUnitsPerDevPixel = mOuter->PresContext()->AppUnitsPerDevPixel(); + deltaMultiplier = nsSize(appUnitsPerDevPixel, appUnitsPerDevPixel); + negativeTolerance = 0.1f; + positiveTolerance = 0; + nsIScrollableFrame::ScrollUnit snapUnit = aUnit; + if (aOrigin == nsGkAtoms::mouseWheel) { + // When using a clicky scroll wheel, snap point selection works the same + // as keyboard up/down/left/right navigation, but with varying amounts + // of scroll delta. + snapUnit = nsIScrollableFrame::LINES; + } + GetSnapPointForDestination(snapUnit, mDestination, newPos); + } + } + + // Calculate desired range values. + nscoord rangeLowerX, rangeUpperX, rangeLowerY, rangeUpperY; + CalcRangeForScrollBy(aDelta.x, newPos.x, negativeTolerance, positiveTolerance, + deltaMultiplier.width, &rangeLowerX, &rangeUpperX); + CalcRangeForScrollBy(aDelta.y, newPos.y, negativeTolerance, positiveTolerance, + deltaMultiplier.height, &rangeLowerY, &rangeUpperY); + nsRect range(rangeLowerX, + rangeLowerY, + rangeUpperX - rangeLowerX, + rangeUpperY - rangeLowerY); + nsWeakFrame weakFrame(mOuter); + ScrollToWithOrigin(newPos, aMode, aOrigin, &range); + if (!weakFrame.IsAlive()) { + return; + } + + if (aOverflow) { + nsPoint clampAmount = newPos - mDestination; + float appUnitsPerDevPixel = mOuter->PresContext()->AppUnitsPerDevPixel(); + *aOverflow = nsIntPoint( + NSAppUnitsToIntPixels(clampAmount.x, appUnitsPerDevPixel), + NSAppUnitsToIntPixels(clampAmount.y, appUnitsPerDevPixel)); + } + + if (aUnit == nsIScrollableFrame::DEVICE_PIXELS && + !nsLayoutUtils::AsyncPanZoomEnabled(mOuter)) + { + // When APZ is disabled, we must track the velocity + // on the main thread; otherwise, the APZC will manage this. + mVelocityQueue.Sample(GetScrollPosition()); + } +} + +void +ScrollFrameHelper::ScrollSnap(nsIScrollableFrame::ScrollMode aMode) +{ + float flingSensitivity = gfxPrefs::ScrollSnapPredictionSensitivity(); + int maxVelocity = gfxPrefs::ScrollSnapPredictionMaxVelocity(); + maxVelocity = nsPresContext::CSSPixelsToAppUnits(maxVelocity); + int maxOffset = maxVelocity * flingSensitivity; + nsPoint velocity = mVelocityQueue.GetVelocity(); + // Multiply each component individually to avoid integer multiply + nsPoint predictedOffset = nsPoint(velocity.x * flingSensitivity, + velocity.y * flingSensitivity); + predictedOffset.Clamp(maxOffset); + nsPoint pos = GetScrollPosition(); + nsPoint destinationPos = pos + predictedOffset; + ScrollSnap(destinationPos, aMode); +} + +void +ScrollFrameHelper::ScrollSnap(const nsPoint &aDestination, + nsIScrollableFrame::ScrollMode aMode) +{ + nsRect scrollRange = GetScrollRangeForClamping(); + nsPoint pos = GetScrollPosition(); + nsPoint snapDestination = scrollRange.ClampPoint(aDestination); + if (GetSnapPointForDestination(nsIScrollableFrame::DEVICE_PIXELS, + pos, + snapDestination)) { + ScrollTo(snapDestination, aMode); + } +} + +nsSize +ScrollFrameHelper::GetLineScrollAmount() const +{ + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetInflatedFontMetricsForFrame(mOuter); + NS_ASSERTION(fm, "FontMetrics is null, assuming fontHeight == 1 appunit"); + static nscoord sMinLineScrollAmountInPixels = -1; + if (sMinLineScrollAmountInPixels < 0) { + Preferences::AddIntVarCache(&sMinLineScrollAmountInPixels, + "mousewheel.min_line_scroll_amount", 1); + } + int32_t appUnitsPerDevPixel = mOuter->PresContext()->AppUnitsPerDevPixel(); + nscoord minScrollAmountInAppUnits = + std::max(1, sMinLineScrollAmountInPixels) * appUnitsPerDevPixel; + nscoord horizontalAmount = fm ? fm->AveCharWidth() : 0; + nscoord verticalAmount = fm ? fm->MaxHeight() : 0; + return nsSize(std::max(horizontalAmount, minScrollAmountInAppUnits), + std::max(verticalAmount, minScrollAmountInAppUnits)); +} + +/** + * Compute the scrollport size excluding any fixed-pos headers and + * footers. A header or footer is an box that spans that entire width + * of the viewport and touches the top (or bottom, respectively) of the + * viewport. We also want to consider fixed elements that stack or overlap + * to effectively create a larger header or footer. Headers and footers that + * cover more than a third of the the viewport are ignored since they + * probably aren't true headers and footers and we don't want to restrict + * scrolling too much in such cases. This is a bit conservative --- some + * pages use elements as headers or footers that don't span the entire width + * of the viewport --- but it should be a good start. + */ +struct TopAndBottom +{ + TopAndBottom(nscoord aTop, nscoord aBottom) : top(aTop), bottom(aBottom) {} + + nscoord top, bottom; +}; +struct TopComparator +{ + bool Equals(const TopAndBottom& A, const TopAndBottom& B) const { + return A.top == B.top; + } + bool LessThan(const TopAndBottom& A, const TopAndBottom& B) const { + return A.top < B.top; + } +}; +struct ReverseBottomComparator +{ + bool Equals(const TopAndBottom& A, const TopAndBottom& B) const { + return A.bottom == B.bottom; + } + bool LessThan(const TopAndBottom& A, const TopAndBottom& B) const { + return A.bottom > B.bottom; + } +}; +static nsSize +GetScrollPortSizeExcludingHeadersAndFooters(nsIFrame* aViewportFrame, + const nsRect& aScrollPort) +{ + nsTArray<TopAndBottom> list; + nsFrameList fixedFrames = aViewportFrame->GetChildList(nsIFrame::kFixedList); + for (nsFrameList::Enumerator iterator(fixedFrames); !iterator.AtEnd(); + iterator.Next()) { + nsIFrame* f = iterator.get(); + nsRect r = f->GetRectRelativeToSelf(); + r = nsLayoutUtils::TransformFrameRectToAncestor(f, r, aViewportFrame); + r = r.Intersect(aScrollPort); + if ((r.width >= aScrollPort.width / 2 || + r.width >= NSIntPixelsToAppUnits(800, AppUnitsPerCSSPixel())) && + r.height <= aScrollPort.height/3) { + list.AppendElement(TopAndBottom(r.y, r.YMost())); + } + } + + list.Sort(TopComparator()); + nscoord headerBottom = 0; + for (uint32_t i = 0; i < list.Length(); ++i) { + if (list[i].top <= headerBottom) { + headerBottom = std::max(headerBottom, list[i].bottom); + } + } + + list.Sort(ReverseBottomComparator()); + nscoord footerTop = aScrollPort.height; + for (uint32_t i = 0; i < list.Length(); ++i) { + if (list[i].bottom >= footerTop) { + footerTop = std::min(footerTop, list[i].top); + } + } + + headerBottom = std::min(aScrollPort.height/3, headerBottom); + footerTop = std::max(aScrollPort.height - aScrollPort.height/3, footerTop); + + return nsSize(aScrollPort.width, footerTop - headerBottom); +} + +nsSize +ScrollFrameHelper::GetPageScrollAmount() const +{ + nsSize lineScrollAmount = GetLineScrollAmount(); + nsSize effectiveScrollPortSize; + if (mIsRoot) { + // Reduce effective scrollport height by the height of any fixed-pos + // headers or footers + nsIFrame* root = mOuter->PresContext()->PresShell()->GetRootFrame(); + effectiveScrollPortSize = + GetScrollPortSizeExcludingHeadersAndFooters(root, mScrollPort); + } else { + effectiveScrollPortSize = mScrollPort.Size(); + } + // The page increment is the size of the page, minus the smaller of + // 10% of the size or 2 lines. + return nsSize( + effectiveScrollPortSize.width - + std::min(effectiveScrollPortSize.width/10, 2*lineScrollAmount.width), + effectiveScrollPortSize.height - + std::min(effectiveScrollPortSize.height/10, 2*lineScrollAmount.height)); +} + + /** + * this code is resposible for restoring the scroll position back to some + * saved position. if the user has not moved the scroll position manually + * we keep scrolling down until we get to our original position. keep in + * mind that content could incrementally be coming in. we only want to stop + * when we reach our new position. + */ +void +ScrollFrameHelper::ScrollToRestoredPosition() +{ + if (mRestorePos.y == -1 || mLastPos.x == -1 || mLastPos.y == -1) { + return; + } + // make sure our scroll position did not change for where we last put + // it. if it does then the user must have moved it, and we no longer + // need to restore. + // + // In the RTL case, we check whether the scroll position changed using the + // logical scroll position, but we scroll to the physical scroll position in + // all cases + + // if we didn't move, we still need to restore + if (GetLogicalScrollPosition() == mLastPos) { + // if our desired position is different to the scroll position, scroll. + // remember that we could be incrementally loading so we may enter + // and scroll many times. + if (mRestorePos != mLastPos /* GetLogicalScrollPosition() */) { + nsPoint scrollToPos = mRestorePos; + if (!IsPhysicalLTR()) { + // convert from logical to physical scroll position + scrollToPos.x = mScrollPort.x - + (mScrollPort.XMost() - scrollToPos.x - mScrolledFrame->GetRect().width); + } + nsWeakFrame weakFrame(mOuter); + ScrollToWithOrigin(scrollToPos, nsIScrollableFrame::INSTANT, + nsGkAtoms::restore, nullptr); + if (!weakFrame.IsAlive()) { + return; + } + if (PageIsStillLoading() || NS_SUBTREE_DIRTY(mOuter)) { + // If we're trying to do a history scroll restore, then we want to + // keep trying this until we succeed, because the page can be loading + // incrementally. So re-get the scroll position for the next iteration, + // it might not be exactly equal to mRestorePos due to rounding and + // clamping. + mLastPos = GetLogicalScrollPosition(); + return; + } + } + // If we get here, either we reached the desired position (mLastPos == + // mRestorePos) or we're not trying to do a history scroll restore, so + // we can stop after the scroll attempt above. + mRestorePos.y = -1; + mLastPos.x = -1; + mLastPos.y = -1; + } else { + // user moved the position, so we won't need to restore + mLastPos.x = -1; + mLastPos.y = -1; + } +} + +bool +ScrollFrameHelper::PageIsStillLoading() +{ + bool loadCompleted = false; + nsCOMPtr<nsIDocShell> ds = mOuter->GetContent()->GetComposedDoc()->GetDocShell(); + if (ds) { + nsCOMPtr<nsIContentViewer> cv; + ds->GetContentViewer(getter_AddRefs(cv)); + cv->GetLoadCompleted(&loadCompleted); + } + return !loadCompleted; +} + +nsresult +ScrollFrameHelper::FireScrollPortEvent() +{ + mAsyncScrollPortEvent.Forget(); + + // Keep this in sync with PostOverflowEvent(). + nsSize scrollportSize = mScrollPort.Size(); + nsSize childSize = GetScrolledRect().Size(); + + bool newVerticalOverflow = childSize.height > scrollportSize.height; + bool vertChanged = mVerticalOverflow != newVerticalOverflow; + + bool newHorizontalOverflow = childSize.width > scrollportSize.width; + bool horizChanged = mHorizontalOverflow != newHorizontalOverflow; + + if (!vertChanged && !horizChanged) { + return NS_OK; + } + + // If both either overflowed or underflowed then we dispatch only one + // DOM event. + bool both = vertChanged && horizChanged && + newVerticalOverflow == newHorizontalOverflow; + InternalScrollPortEvent::OrientType orient; + if (both) { + orient = InternalScrollPortEvent::eBoth; + mHorizontalOverflow = newHorizontalOverflow; + mVerticalOverflow = newVerticalOverflow; + } + else if (vertChanged) { + orient = InternalScrollPortEvent::eVertical; + mVerticalOverflow = newVerticalOverflow; + if (horizChanged) { + // We need to dispatch a separate horizontal DOM event. Do that the next + // time around since dispatching the vertical DOM event might destroy + // the frame. + PostOverflowEvent(); + } + } + else { + orient = InternalScrollPortEvent::eHorizontal; + mHorizontalOverflow = newHorizontalOverflow; + } + + InternalScrollPortEvent event(true, + (orient == InternalScrollPortEvent::eHorizontal ? mHorizontalOverflow : + mVerticalOverflow) ? + eScrollPortOverflow : eScrollPortUnderflow, nullptr); + event.mOrient = orient; + return EventDispatcher::Dispatch(mOuter->GetContent(), + mOuter->PresContext(), &event); +} + +void +ScrollFrameHelper::ReloadChildFrames() +{ + mScrolledFrame = nullptr; + mHScrollbarBox = nullptr; + mVScrollbarBox = nullptr; + mScrollCornerBox = nullptr; + mResizerBox = nullptr; + + for (nsIFrame* frame : mOuter->PrincipalChildList()) { + nsIContent* content = frame->GetContent(); + if (content == mOuter->GetContent()) { + NS_ASSERTION(!mScrolledFrame, "Already found the scrolled frame"); + mScrolledFrame = frame; + } else { + nsAutoString value; + content->GetAttr(kNameSpaceID_None, nsGkAtoms::orient, value); + if (!value.IsEmpty()) { + // probably a scrollbar then + if (value.LowerCaseEqualsLiteral("horizontal")) { + NS_ASSERTION(!mHScrollbarBox, "Found multiple horizontal scrollbars?"); + mHScrollbarBox = frame; + } else { + NS_ASSERTION(!mVScrollbarBox, "Found multiple vertical scrollbars?"); + mVScrollbarBox = frame; + } + } else if (content->IsXULElement(nsGkAtoms::resizer)) { + NS_ASSERTION(!mResizerBox, "Found multiple resizers"); + mResizerBox = frame; + } else if (content->IsXULElement(nsGkAtoms::scrollcorner)) { + // probably a scrollcorner + NS_ASSERTION(!mScrollCornerBox, "Found multiple scrollcorners"); + mScrollCornerBox = frame; + } + } + } +} + +nsresult +ScrollFrameHelper::CreateAnonymousContent( + nsTArray<nsIAnonymousContentCreator::ContentInfo>& aElements) +{ + nsPresContext* presContext = mOuter->PresContext(); + nsIFrame* parent = mOuter->GetParent(); + + // Don't create scrollbars if we're an SVG document being used as an image, + // or if we're printing/print previewing. + // (In the printing case, we allow scrollbars if this is the child of the + // viewport & paginated scrolling is enabled, because then we must be the + // scroll frame for the print preview window, & that does need scrollbars.) + if (presContext->Document()->IsBeingUsedAsImage() || + (!presContext->IsDynamic() && + !(mIsRoot && presContext->HasPaginatedScrolling()))) { + mNeverHasVerticalScrollbar = mNeverHasHorizontalScrollbar = true; + return NS_OK; + } + + // Check if the frame is resizable. Note: + // "The effect of the resize property on generated content is undefined. + // Implementations should not apply the resize property to generated + // content." [1] + // For info on what is generated content, see [2]. + // [1]: https://drafts.csswg.org/css-ui/#resize + // [2]: https://www.w3.org/TR/CSS2/generate.html#content + int8_t resizeStyle = mOuter->StyleDisplay()->mResize; + bool isResizable = resizeStyle != NS_STYLE_RESIZE_NONE && + !mOuter->HasAnyStateBits(NS_FRAME_GENERATED_CONTENT); + + nsIScrollableFrame *scrollable = do_QueryFrame(mOuter); + + // If we're the scrollframe for the root, then we want to construct + // our scrollbar frames no matter what. That way later dynamic + // changes to propagated overflow styles will show or hide + // scrollbars on the viewport without requiring frame reconstruction + // of the viewport (good!). + bool canHaveHorizontal; + bool canHaveVertical; + if (!mIsRoot) { + ScrollbarStyles styles = scrollable->GetScrollbarStyles(); + canHaveHorizontal = styles.mHorizontal != NS_STYLE_OVERFLOW_HIDDEN; + canHaveVertical = styles.mVertical != NS_STYLE_OVERFLOW_HIDDEN; + if (!canHaveHorizontal && !canHaveVertical && !isResizable) { + // Nothing to do. + return NS_OK; + } + } else { + canHaveHorizontal = true; + canHaveVertical = true; + } + + // The anonymous <div> used by <inputs> never gets scrollbars. + nsITextControlFrame* textFrame = do_QueryFrame(parent); + if (textFrame) { + // Make sure we are not a text area. + nsCOMPtr<nsIDOMHTMLTextAreaElement> textAreaElement(do_QueryInterface(parent->GetContent())); + if (!textAreaElement) { + mNeverHasVerticalScrollbar = mNeverHasHorizontalScrollbar = true; + return NS_OK; + } + } + + nsNodeInfoManager *nodeInfoManager = + presContext->Document()->NodeInfoManager(); + RefPtr<NodeInfo> nodeInfo; + nodeInfo = nodeInfoManager->GetNodeInfo(nsGkAtoms::scrollbar, nullptr, + kNameSpaceID_XUL, + nsIDOMNode::ELEMENT_NODE); + NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY); + + if (canHaveHorizontal) { + RefPtr<NodeInfo> ni = nodeInfo; + NS_TrustedNewXULElement(getter_AddRefs(mHScrollbarContent), ni.forget()); +#ifdef DEBUG + // Scrollbars can get restyled by theme changes. Whether such a restyle + // will actually reconstruct them correctly if it involves a frame + // reconstruct... I don't know. :( + mHScrollbarContent->SetProperty(nsGkAtoms::restylableAnonymousNode, + reinterpret_cast<void*>(true)); +#endif // DEBUG + + mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::orient, + NS_LITERAL_STRING("horizontal"), false); + mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::clickthrough, + NS_LITERAL_STRING("always"), false); + if (mIsRoot) { + mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::root_, + NS_LITERAL_STRING("true"), false); + } + if (!aElements.AppendElement(mHScrollbarContent)) + return NS_ERROR_OUT_OF_MEMORY; + } + + if (canHaveVertical) { + RefPtr<NodeInfo> ni = nodeInfo; + NS_TrustedNewXULElement(getter_AddRefs(mVScrollbarContent), ni.forget()); +#ifdef DEBUG + // Scrollbars can get restyled by theme changes. Whether such a restyle + // will actually reconstruct them correctly if it involves a frame + // reconstruct... I don't know. :( + mVScrollbarContent->SetProperty(nsGkAtoms::restylableAnonymousNode, + reinterpret_cast<void*>(true)); +#endif // DEBUG + + mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::orient, + NS_LITERAL_STRING("vertical"), false); + mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::clickthrough, + NS_LITERAL_STRING("always"), false); + if (mIsRoot) { + mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::root_, + NS_LITERAL_STRING("true"), false); + } + if (!aElements.AppendElement(mVScrollbarContent)) + return NS_ERROR_OUT_OF_MEMORY; + } + + if (isResizable) { + RefPtr<NodeInfo> nodeInfo; + nodeInfo = nodeInfoManager->GetNodeInfo(nsGkAtoms::resizer, nullptr, + kNameSpaceID_XUL, + nsIDOMNode::ELEMENT_NODE); + NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY); + + NS_TrustedNewXULElement(getter_AddRefs(mResizerContent), nodeInfo.forget()); + + nsAutoString dir; + switch (resizeStyle) { + case NS_STYLE_RESIZE_HORIZONTAL: + if (IsScrollbarOnRight()) { + dir.AssignLiteral("right"); + } + else { + dir.AssignLiteral("left"); + } + break; + case NS_STYLE_RESIZE_VERTICAL: + dir.AssignLiteral("bottom"); + break; + case NS_STYLE_RESIZE_BOTH: + dir.AssignLiteral("bottomend"); + break; + default: + NS_WARNING("only resizable types should have resizers"); + } + mResizerContent->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, dir, false); + + if (mIsRoot) { + nsIContent* browserRoot = GetBrowserRoot(mOuter->GetContent()); + mCollapsedResizer = !(browserRoot && + browserRoot->HasAttr(kNameSpaceID_None, nsGkAtoms::showresizer)); + } + else { + mResizerContent->SetAttr(kNameSpaceID_None, nsGkAtoms::element, + NS_LITERAL_STRING("_parent"), false); + } + + mResizerContent->SetAttr(kNameSpaceID_None, nsGkAtoms::clickthrough, + NS_LITERAL_STRING("always"), false); + + if (!aElements.AppendElement(mResizerContent)) + return NS_ERROR_OUT_OF_MEMORY; + } + + if (canHaveHorizontal && canHaveVertical) { + nodeInfo = nodeInfoManager->GetNodeInfo(nsGkAtoms::scrollcorner, nullptr, + kNameSpaceID_XUL, + nsIDOMNode::ELEMENT_NODE); + NS_TrustedNewXULElement(getter_AddRefs(mScrollCornerContent), nodeInfo.forget()); + if (!aElements.AppendElement(mScrollCornerContent)) + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void +ScrollFrameHelper::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, + uint32_t aFilter) +{ + if (mHScrollbarContent) { + aElements.AppendElement(mHScrollbarContent); + } + + if (mVScrollbarContent) { + aElements.AppendElement(mVScrollbarContent); + } + + if (mScrollCornerContent) { + aElements.AppendElement(mScrollCornerContent); + } + + if (mResizerContent) { + aElements.AppendElement(mResizerContent); + } +} + +void +ScrollFrameHelper::Destroy() +{ + if (mScrollbarActivity) { + mScrollbarActivity->Destroy(); + mScrollbarActivity = nullptr; + } + + // Unbind any content created in CreateAnonymousContent from the tree + nsContentUtils::DestroyAnonymousContent(&mHScrollbarContent); + nsContentUtils::DestroyAnonymousContent(&mVScrollbarContent); + nsContentUtils::DestroyAnonymousContent(&mScrollCornerContent); + nsContentUtils::DestroyAnonymousContent(&mResizerContent); + + if (mPostedReflowCallback) { + mOuter->PresContext()->PresShell()->CancelReflowCallback(this); + mPostedReflowCallback = false; + } + + if (mDisplayPortExpiryTimer) { + mDisplayPortExpiryTimer->Cancel(); + mDisplayPortExpiryTimer = nullptr; + } + if (mActivityExpirationState.IsTracked()) { + gScrollFrameActivityTracker->RemoveObject(this); + } + if (gScrollFrameActivityTracker && + gScrollFrameActivityTracker->IsEmpty()) { + delete gScrollFrameActivityTracker; + gScrollFrameActivityTracker = nullptr; + } + + if (mScrollActivityTimer) { + mScrollActivityTimer->Cancel(); + mScrollActivityTimer = nullptr; + } +} + +/** + * Called when we want to update the scrollbar position, either because scrolling happened + * or the user moved the scrollbar position and we need to undo that (e.g., when the user + * clicks to scroll and we're using smooth scrolling, so we need to put the thumb back + * to its initial position for the start of the smooth sequence). + */ +void +ScrollFrameHelper::UpdateScrollbarPosition() +{ + nsWeakFrame weakFrame(mOuter); + mFrameIsUpdatingScrollbar = true; + + nsPoint pt = GetScrollPosition(); + if (mVScrollbarBox) { + SetCoordAttribute(mVScrollbarBox->GetContent(), nsGkAtoms::curpos, + pt.y - GetScrolledRect().y); + if (!weakFrame.IsAlive()) { + return; + } + } + if (mHScrollbarBox) { + SetCoordAttribute(mHScrollbarBox->GetContent(), nsGkAtoms::curpos, + pt.x - GetScrolledRect().x); + if (!weakFrame.IsAlive()) { + return; + } + } + + mFrameIsUpdatingScrollbar = false; +} + +void ScrollFrameHelper::CurPosAttributeChanged(nsIContent* aContent) +{ + NS_ASSERTION(aContent, "aContent must not be null"); + NS_ASSERTION((mHScrollbarBox && mHScrollbarBox->GetContent() == aContent) || + (mVScrollbarBox && mVScrollbarBox->GetContent() == aContent), + "unexpected child"); + + // Attribute changes on the scrollbars happen in one of three ways: + // 1) The scrollbar changed the attribute in response to some user event + // 2) We changed the attribute in response to a ScrollPositionDidChange + // callback from the scrolling view + // 3) We changed the attribute to adjust the scrollbars for the start + // of a smooth scroll operation + // + // In cases 2 and 3 we do not need to scroll because we're just + // updating our scrollbar. + if (mFrameIsUpdatingScrollbar) + return; + + nsRect scrolledRect = GetScrolledRect(); + + nsPoint current = GetScrollPosition() - scrolledRect.TopLeft(); + nsPoint dest; + nsRect allowedRange; + dest.x = GetCoordAttribute(mHScrollbarBox, nsGkAtoms::curpos, current.x, + &allowedRange.x, &allowedRange.width); + dest.y = GetCoordAttribute(mVScrollbarBox, nsGkAtoms::curpos, current.y, + &allowedRange.y, &allowedRange.height); + current += scrolledRect.TopLeft(); + dest += scrolledRect.TopLeft(); + allowedRange += scrolledRect.TopLeft(); + + // Don't try to scroll if we're already at an acceptable place. + // Don't call Contains here since Contains returns false when the point is + // on the bottom or right edge of the rectangle. + if (allowedRange.ClampPoint(current) == current) { + return; + } + + if (mScrollbarActivity) { + RefPtr<ScrollbarActivity> scrollbarActivity(mScrollbarActivity); + scrollbarActivity->ActivityOccurred(); + } + + bool isSmooth = aContent->HasAttr(kNameSpaceID_None, nsGkAtoms::smooth); + if (isSmooth) { + // Make sure an attribute-setting callback occurs even if the view + // didn't actually move yet. We need to make sure other listeners + // see that the scroll position is not (yet) what they thought it + // was. + nsWeakFrame weakFrame(mOuter); + UpdateScrollbarPosition(); + if (!weakFrame.IsAlive()) { + return; + } + } + ScrollToWithOrigin(dest, + isSmooth ? nsIScrollableFrame::SMOOTH : nsIScrollableFrame::INSTANT, + nsGkAtoms::scrollbars, &allowedRange); + // 'this' might be destroyed here +} + +/* ============= Scroll events ========== */ + +ScrollFrameHelper::ScrollEvent::ScrollEvent(ScrollFrameHelper* aHelper) + : mHelper(aHelper) +{ + mDriver = mHelper->mOuter->PresContext()->RefreshDriver(); + mDriver->AddRefreshObserver(this, Flush_Layout); +} + +ScrollFrameHelper::ScrollEvent::~ScrollEvent() +{ + if (mDriver) { + mDriver->RemoveRefreshObserver(this, Flush_Layout); + mDriver = nullptr; + } +} + +void +ScrollFrameHelper::ScrollEvent::WillRefresh(mozilla::TimeStamp aTime) +{ + mDriver->RemoveRefreshObserver(this, Flush_Layout); + mDriver = nullptr; + mHelper->FireScrollEvent(); +} + +void +ScrollFrameHelper::FireScrollEvent() +{ + MOZ_ASSERT(mScrollEvent); + mScrollEvent = nullptr; + + ActiveLayerTracker::SetCurrentScrollHandlerFrame(mOuter); + WidgetGUIEvent event(true, eScroll, nullptr); + nsEventStatus status = nsEventStatus_eIgnore; + nsIContent* content = mOuter->GetContent(); + nsPresContext* prescontext = mOuter->PresContext(); + // Fire viewport scroll events at the document (where they + // will bubble to the window) + mozilla::layers::ScrollLinkedEffectDetector detector(content->GetComposedDoc()); + if (mIsRoot) { + nsIDocument* doc = content->GetUncomposedDoc(); + if (doc) { + EventDispatcher::Dispatch(doc, prescontext, &event, nullptr, &status); + } + } else { + // scroll events fired at elements don't bubble (although scroll events + // fired at documents do, to the window) + event.mFlags.mBubbles = false; + EventDispatcher::Dispatch(content, prescontext, &event, nullptr, &status); + } + ActiveLayerTracker::SetCurrentScrollHandlerFrame(nullptr); +} + +void +ScrollFrameHelper::PostScrollEvent() +{ + if (mScrollEvent) + return; + + // The ScrollEvent constructor registers itself with the refresh driver. + mScrollEvent = new ScrollEvent(this); +} + +NS_IMETHODIMP +ScrollFrameHelper::AsyncScrollPortEvent::Run() +{ + if (mHelper) { + mHelper->mOuter->PresContext()->GetPresShell()-> + FlushPendingNotifications(Flush_InterruptibleLayout); + } + return mHelper ? mHelper->FireScrollPortEvent() : NS_OK; +} + +bool +nsXULScrollFrame::AddHorizontalScrollbar(nsBoxLayoutState& aState, bool aOnBottom) +{ + if (!mHelper.mHScrollbarBox) + return true; + + return AddRemoveScrollbar(aState, aOnBottom, true, true); +} + +bool +nsXULScrollFrame::AddVerticalScrollbar(nsBoxLayoutState& aState, bool aOnRight) +{ + if (!mHelper.mVScrollbarBox) + return true; + + return AddRemoveScrollbar(aState, aOnRight, false, true); +} + +void +nsXULScrollFrame::RemoveHorizontalScrollbar(nsBoxLayoutState& aState, bool aOnBottom) +{ + // removing a scrollbar should always fit + DebugOnly<bool> result = AddRemoveScrollbar(aState, aOnBottom, true, false); + NS_ASSERTION(result, "Removing horizontal scrollbar failed to fit??"); +} + +void +nsXULScrollFrame::RemoveVerticalScrollbar(nsBoxLayoutState& aState, bool aOnRight) +{ + // removing a scrollbar should always fit + DebugOnly<bool> result = AddRemoveScrollbar(aState, aOnRight, false, false); + NS_ASSERTION(result, "Removing vertical scrollbar failed to fit??"); +} + +bool +nsXULScrollFrame::AddRemoveScrollbar(nsBoxLayoutState& aState, + bool aOnRightOrBottom, bool aHorizontal, bool aAdd) +{ + if (aHorizontal) { + if (mHelper.mNeverHasHorizontalScrollbar || !mHelper.mHScrollbarBox) + return false; + + nsSize hSize = mHelper.mHScrollbarBox->GetXULPrefSize(aState); + nsBox::AddMargin(mHelper.mHScrollbarBox, hSize); + + mHelper.SetScrollbarVisibility(mHelper.mHScrollbarBox, aAdd); + + bool hasHorizontalScrollbar; + bool fit = AddRemoveScrollbar(hasHorizontalScrollbar, + mHelper.mScrollPort.y, + mHelper.mScrollPort.height, + hSize.height, aOnRightOrBottom, aAdd); + mHelper.mHasHorizontalScrollbar = hasHorizontalScrollbar; // because mHasHorizontalScrollbar is a bool + if (!fit) + mHelper.SetScrollbarVisibility(mHelper.mHScrollbarBox, !aAdd); + + return fit; + } else { + if (mHelper.mNeverHasVerticalScrollbar || !mHelper.mVScrollbarBox) + return false; + + nsSize vSize = mHelper.mVScrollbarBox->GetXULPrefSize(aState); + nsBox::AddMargin(mHelper.mVScrollbarBox, vSize); + + mHelper.SetScrollbarVisibility(mHelper.mVScrollbarBox, aAdd); + + bool hasVerticalScrollbar; + bool fit = AddRemoveScrollbar(hasVerticalScrollbar, + mHelper.mScrollPort.x, + mHelper.mScrollPort.width, + vSize.width, aOnRightOrBottom, aAdd); + mHelper.mHasVerticalScrollbar = hasVerticalScrollbar; // because mHasVerticalScrollbar is a bool + if (!fit) + mHelper.SetScrollbarVisibility(mHelper.mVScrollbarBox, !aAdd); + + return fit; + } +} + +bool +nsXULScrollFrame::AddRemoveScrollbar(bool& aHasScrollbar, nscoord& aXY, + nscoord& aSize, nscoord aSbSize, + bool aOnRightOrBottom, bool aAdd) +{ + nscoord size = aSize; + nscoord xy = aXY; + + if (size != NS_INTRINSICSIZE) { + if (aAdd) { + size -= aSbSize; + if (!aOnRightOrBottom && size >= 0) + xy += aSbSize; + } else { + size += aSbSize; + if (!aOnRightOrBottom) + xy -= aSbSize; + } + } + + // not enough room? Yes? Return true. + if (size >= 0) { + aHasScrollbar = aAdd; + aSize = size; + aXY = xy; + return true; + } + + aHasScrollbar = false; + return false; +} + +void +nsXULScrollFrame::LayoutScrollArea(nsBoxLayoutState& aState, + const nsPoint& aScrollPosition) +{ + uint32_t oldflags = aState.LayoutFlags(); + nsRect childRect = nsRect(mHelper.mScrollPort.TopLeft() - aScrollPosition, + mHelper.mScrollPort.Size()); + int32_t flags = NS_FRAME_NO_MOVE_VIEW; + + nsSize minSize = mHelper.mScrolledFrame->GetXULMinSize(aState); + + if (minSize.height > childRect.height) + childRect.height = minSize.height; + + if (minSize.width > childRect.width) + childRect.width = minSize.width; + + // TODO: Handle transformed children that inherit perspective + // from this frame. See AdjustForPerspective for how we handle + // this for HTML scroll frames. + + aState.SetLayoutFlags(flags); + ClampAndSetBounds(aState, childRect, aScrollPosition); + mHelper.mScrolledFrame->XULLayout(aState); + + childRect = mHelper.mScrolledFrame->GetRect(); + + if (childRect.width < mHelper.mScrollPort.width || + childRect.height < mHelper.mScrollPort.height) + { + childRect.width = std::max(childRect.width, mHelper.mScrollPort.width); + childRect.height = std::max(childRect.height, mHelper.mScrollPort.height); + + // remove overflow areas when we update the bounds, + // because we've already accounted for it + // REVIEW: Have we accounted for both? + ClampAndSetBounds(aState, childRect, aScrollPosition, true); + } + + aState.SetLayoutFlags(oldflags); + +} + +void ScrollFrameHelper::PostOverflowEvent() +{ + if (mAsyncScrollPortEvent.IsPending()) + return; + + // Keep this in sync with FireScrollPortEvent(). + nsSize scrollportSize = mScrollPort.Size(); + nsSize childSize = GetScrolledRect().Size(); + + bool newVerticalOverflow = childSize.height > scrollportSize.height; + bool vertChanged = mVerticalOverflow != newVerticalOverflow; + + bool newHorizontalOverflow = childSize.width > scrollportSize.width; + bool horizChanged = mHorizontalOverflow != newHorizontalOverflow; + + if (!vertChanged && !horizChanged) { + return; + } + + nsRootPresContext* rpc = mOuter->PresContext()->GetRootPresContext(); + if (!rpc) + return; + + mAsyncScrollPortEvent = new AsyncScrollPortEvent(this); + rpc->AddWillPaintObserver(mAsyncScrollPortEvent.get()); +} + +nsIFrame* +ScrollFrameHelper::GetFrameForDir() const +{ + nsIFrame *frame = mOuter; + // XXX This is a bit on the slow side. + if (mIsRoot) { + // If we're the root scrollframe, we need the root element's style data. + nsPresContext *presContext = mOuter->PresContext(); + nsIDocument *document = presContext->Document(); + Element *root = document->GetRootElement(); + + // But for HTML and XHTML we want the body element. + nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(document); + if (htmlDoc) { + Element *bodyElement = document->GetBodyElement(); + if (bodyElement) + root = bodyElement; // we can trust the document to hold on to it + } + + if (root) { + nsIFrame *rootsFrame = root->GetPrimaryFrame(); + if (rootsFrame) + frame = rootsFrame; + } + } + + return frame; +} + +bool +ScrollFrameHelper::IsScrollbarOnRight() const +{ + nsPresContext *presContext = mOuter->PresContext(); + + // The position of the scrollbar in top-level windows depends on the pref + // layout.scrollbar.side. For non-top-level elements, it depends only on the + // directionaliy of the element (equivalent to a value of "1" for the pref). + if (!mIsRoot) { + return IsPhysicalLTR(); + } + switch (presContext->GetCachedIntPref(kPresContext_ScrollbarSide)) { + default: + case 0: // UI directionality + return presContext->GetCachedIntPref(kPresContext_BidiDirection) + == IBMBIDI_TEXTDIRECTION_LTR; + case 1: // Document / content directionality + return IsPhysicalLTR(); + case 2: // Always right + return true; + case 3: // Always left + return false; + } +} + +bool +ScrollFrameHelper::IsMaybeScrollingActive() const +{ + const nsStyleDisplay* disp = mOuter->StyleDisplay(); + if (disp && (disp->mWillChangeBitField & NS_STYLE_WILL_CHANGE_SCROLL)) { + return true; + } + + return mHasBeenScrolledRecently || + IsAlwaysActive() || + mWillBuildScrollableLayer; +} + +bool +ScrollFrameHelper::IsScrollingActive(nsDisplayListBuilder* aBuilder) const +{ + const nsStyleDisplay* disp = mOuter->StyleDisplay(); + if (disp && (disp->mWillChangeBitField & NS_STYLE_WILL_CHANGE_SCROLL) && + aBuilder->IsInWillChangeBudget(mOuter, GetScrollPositionClampingScrollPortSize())) { + return true; + } + + return mHasBeenScrolledRecently || + IsAlwaysActive() || + mWillBuildScrollableLayer; +} + +/** + * Reflow the scroll area if it needs it and return its size. Also determine if the reflow will + * cause any of the scrollbars to need to be reflowed. + */ +nsresult +nsXULScrollFrame::XULLayout(nsBoxLayoutState& aState) +{ + bool scrollbarRight = IsScrollbarOnRight(); + bool scrollbarBottom = true; + + // get the content rect + nsRect clientRect(0,0,0,0); + GetXULClientRect(clientRect); + + nsRect oldScrollAreaBounds = mHelper.mScrollPort; + nsPoint oldScrollPosition = mHelper.GetLogicalScrollPosition(); + + // the scroll area size starts off as big as our content area + mHelper.mScrollPort = clientRect; + + /************** + Our basic strategy here is to first try laying out the content with + the scrollbars in their current state. We're hoping that that will + just "work"; the content will overflow wherever there's a scrollbar + already visible. If that does work, then there's no need to lay out + the scrollarea. Otherwise we fix up the scrollbars; first we add a + vertical one to scroll the content if necessary, or remove it if + it's not needed. Then we reflow the content if the scrollbar + changed. Then we add a horizontal scrollbar if necessary (or + remove if not needed), and if that changed, we reflow the content + again. At this point, any scrollbars that are needed to scroll the + content have been added. + + In the second phase we check to see if any scrollbars are too small + to display, and if so, we remove them. We check the horizontal + scrollbar first; removing it might make room for the vertical + scrollbar, and if we have room for just one scrollbar we'll save + the vertical one. + + Finally we position and size the scrollbars and scrollcorner (the + square that is needed in the corner of the window when two + scrollbars are visible), and reflow any fixed position views + (if we're the viewport and we added or removed a scrollbar). + **************/ + + ScrollbarStyles styles = GetScrollbarStyles(); + + // Look at our style do we always have vertical or horizontal scrollbars? + if (styles.mHorizontal == NS_STYLE_OVERFLOW_SCROLL) + mHelper.mHasHorizontalScrollbar = true; + if (styles.mVertical == NS_STYLE_OVERFLOW_SCROLL) + mHelper.mHasVerticalScrollbar = true; + + if (mHelper.mHasHorizontalScrollbar) + AddHorizontalScrollbar(aState, scrollbarBottom); + + if (mHelper.mHasVerticalScrollbar) + AddVerticalScrollbar(aState, scrollbarRight); + + // layout our the scroll area + LayoutScrollArea(aState, oldScrollPosition); + + // now look at the content area and see if we need scrollbars or not + bool needsLayout = false; + + // if we have 'auto' scrollbars look at the vertical case + if (styles.mVertical != NS_STYLE_OVERFLOW_SCROLL) { + // These are only good until the call to LayoutScrollArea. + nsRect scrolledRect = mHelper.GetScrolledRect(); + + // There are two cases to consider + if (scrolledRect.height <= mHelper.mScrollPort.height + || styles.mVertical != NS_STYLE_OVERFLOW_AUTO) { + if (mHelper.mHasVerticalScrollbar) { + // We left room for the vertical scrollbar, but it's not needed; + // remove it. + RemoveVerticalScrollbar(aState, scrollbarRight); + needsLayout = true; + } + } else { + if (!mHelper.mHasVerticalScrollbar) { + // We didn't leave room for the vertical scrollbar, but it turns + // out we needed it + if (AddVerticalScrollbar(aState, scrollbarRight)) + needsLayout = true; + } + } + + // ok layout at the right size + if (needsLayout) { + nsBoxLayoutState resizeState(aState); + LayoutScrollArea(resizeState, oldScrollPosition); + needsLayout = false; + } + } + + + // if scrollbars are auto look at the horizontal case + if (styles.mHorizontal != NS_STYLE_OVERFLOW_SCROLL) + { + // These are only good until the call to LayoutScrollArea. + nsRect scrolledRect = mHelper.GetScrolledRect(); + + // if the child is wider that the scroll area + // and we don't have a scrollbar add one. + if ((scrolledRect.width > mHelper.mScrollPort.width) + && styles.mHorizontal == NS_STYLE_OVERFLOW_AUTO) { + + if (!mHelper.mHasHorizontalScrollbar) { + // no scrollbar? + if (AddHorizontalScrollbar(aState, scrollbarBottom)) + needsLayout = true; + + // if we added a horizontal scrollbar and we did not have a vertical + // there is a chance that by adding the horizontal scrollbar we will + // suddenly need a vertical scrollbar. Is a special case but its + // important. + //if (!mHasVerticalScrollbar && scrolledRect.height > scrollAreaRect.height - sbSize.height) + // printf("****Gfx Scrollbar Special case hit!!*****\n"); + + } + } else { + // if the area is smaller or equal to and we have a scrollbar then + // remove it. + if (mHelper.mHasHorizontalScrollbar) { + RemoveHorizontalScrollbar(aState, scrollbarBottom); + needsLayout = true; + } + } + } + + // we only need to set the rect. The inner child stays the same size. + if (needsLayout) { + nsBoxLayoutState resizeState(aState); + LayoutScrollArea(resizeState, oldScrollPosition); + needsLayout = false; + } + + // get the preferred size of the scrollbars + nsSize hMinSize(0, 0); + if (mHelper.mHScrollbarBox && mHelper.mHasHorizontalScrollbar) { + GetScrollbarMetrics(aState, mHelper.mHScrollbarBox, &hMinSize, nullptr, false); + } + nsSize vMinSize(0, 0); + if (mHelper.mVScrollbarBox && mHelper.mHasVerticalScrollbar) { + GetScrollbarMetrics(aState, mHelper.mVScrollbarBox, &vMinSize, nullptr, true); + } + + // Disable scrollbars that are too small + // Disable horizontal scrollbar first. If we have to disable only one + // scrollbar, we'd rather keep the vertical scrollbar. + // Note that we always give horizontal scrollbars their preferred height, + // never their min-height. So check that there's room for the preferred height. + if (mHelper.mHasHorizontalScrollbar && + (hMinSize.width > clientRect.width - vMinSize.width + || hMinSize.height > clientRect.height)) { + RemoveHorizontalScrollbar(aState, scrollbarBottom); + needsLayout = true; + } + // Now disable vertical scrollbar if necessary + if (mHelper.mHasVerticalScrollbar && + (vMinSize.height > clientRect.height - hMinSize.height + || vMinSize.width > clientRect.width)) { + RemoveVerticalScrollbar(aState, scrollbarRight); + needsLayout = true; + } + + // we only need to set the rect. The inner child stays the same size. + if (needsLayout) { + nsBoxLayoutState resizeState(aState); + LayoutScrollArea(resizeState, oldScrollPosition); + } + + if (!mHelper.mSupppressScrollbarUpdate) { + mHelper.LayoutScrollbars(aState, clientRect, oldScrollAreaBounds); + } + if (!mHelper.mPostedReflowCallback) { + // Make sure we'll try scrolling to restored position + PresContext()->PresShell()->PostReflowCallback(&mHelper); + mHelper.mPostedReflowCallback = true; + } + if (!(GetStateBits() & NS_FRAME_FIRST_REFLOW)) { + mHelper.mHadNonInitialReflow = true; + } + + mHelper.UpdateSticky(); + + // Set up overflow areas for block frames for the benefit of + // text-overflow. + nsIFrame* f = mHelper.mScrolledFrame->GetContentInsertionFrame(); + if (nsLayoutUtils::GetAsBlock(f)) { + nsRect origRect = f->GetRect(); + nsRect clippedRect = origRect; + clippedRect.MoveBy(mHelper.mScrollPort.TopLeft()); + clippedRect.IntersectRect(clippedRect, mHelper.mScrollPort); + nsOverflowAreas overflow = f->GetOverflowAreas(); + f->FinishAndStoreOverflow(overflow, clippedRect.Size()); + clippedRect.MoveTo(origRect.TopLeft()); + f->SetRect(clippedRect); + } + + mHelper.UpdatePrevScrolledRect(); + + mHelper.PostOverflowEvent(); + return NS_OK; +} + +void +ScrollFrameHelper::FinishReflowForScrollbar(nsIContent* aContent, + nscoord aMinXY, nscoord aMaxXY, + nscoord aCurPosXY, + nscoord aPageIncrement, + nscoord aIncrement) +{ + // Scrollbars assume zero is the minimum position, so translate for them. + SetCoordAttribute(aContent, nsGkAtoms::curpos, aCurPosXY - aMinXY); + SetScrollbarEnabled(aContent, aMaxXY - aMinXY); + SetCoordAttribute(aContent, nsGkAtoms::maxpos, aMaxXY - aMinXY); + SetCoordAttribute(aContent, nsGkAtoms::pageincrement, aPageIncrement); + SetCoordAttribute(aContent, nsGkAtoms::increment, aIncrement); +} + +bool +ScrollFrameHelper::ReflowFinished() +{ + mPostedReflowCallback = false; + + if (NS_SUBTREE_DIRTY(mOuter)) { + // We will get another call after the next reflow and scrolling + // later is less janky. + return false; + } + + nsAutoScriptBlocker scriptBlocker; + ScrollToRestoredPosition(); + + // Clamp current scroll position to new bounds. Normally this won't + // do anything. + nsPoint currentScrollPos = GetScrollPosition(); + ScrollToImpl(currentScrollPos, nsRect(currentScrollPos, nsSize(0, 0))); + if (!mAsyncScroll && !mAsyncSmoothMSDScroll && !mApzSmoothScrollDestination) { + // We need to have mDestination track the current scroll position, + // in case it falls outside the new reflow area. mDestination is used + // by ScrollBy as its starting position. + mDestination = GetScrollPosition(); + } + + if (!mUpdateScrollbarAttributes) { + return false; + } + mUpdateScrollbarAttributes = false; + + // Update scrollbar attributes. + nsPresContext* presContext = mOuter->PresContext(); + + if (mMayHaveDirtyFixedChildren) { + mMayHaveDirtyFixedChildren = false; + nsIFrame* parentFrame = mOuter->GetParent(); + for (nsIFrame* fixedChild = + parentFrame->GetChildList(nsIFrame::kFixedList).FirstChild(); + fixedChild; fixedChild = fixedChild->GetNextSibling()) { + // force a reflow of the fixed child + presContext->PresShell()-> + FrameNeedsReflow(fixedChild, nsIPresShell::eResize, + NS_FRAME_HAS_DIRTY_CHILDREN); + } + } + + nsRect scrolledContentRect = GetScrolledRect(); + nsSize scrollClampingScrollPort = GetScrollPositionClampingScrollPortSize(); + nscoord minX = scrolledContentRect.x; + nscoord maxX = scrolledContentRect.XMost() - scrollClampingScrollPort.width; + nscoord minY = scrolledContentRect.y; + nscoord maxY = scrolledContentRect.YMost() - scrollClampingScrollPort.height; + + // Suppress handling of the curpos attribute changes we make here. + NS_ASSERTION(!mFrameIsUpdatingScrollbar, "We shouldn't be reentering here"); + mFrameIsUpdatingScrollbar = true; + + nsCOMPtr<nsIContent> vScroll = + mVScrollbarBox ? mVScrollbarBox->GetContent() : nullptr; + nsCOMPtr<nsIContent> hScroll = + mHScrollbarBox ? mHScrollbarBox->GetContent() : nullptr; + + // Note, in some cases mOuter may get deleted while finishing reflow + // for scrollbars. XXXmats is this still true now that we have a script + // blocker in this scope? (if not, remove the weak frame checks below). + if (vScroll || hScroll) { + nsWeakFrame weakFrame(mOuter); + nsPoint scrollPos = GetScrollPosition(); + nsSize lineScrollAmount = GetLineScrollAmount(); + if (vScroll) { + const double kScrollMultiplier = + Preferences::GetInt("toolkit.scrollbox.verticalScrollDistance", + NS_DEFAULT_VERTICAL_SCROLL_DISTANCE); + nscoord increment = lineScrollAmount.height * kScrollMultiplier; + // We normally use (scrollArea.height - increment) for height + // of page scrolling. However, it is too small when + // increment is very large. (If increment is larger than + // scrollArea.height, direction of scrolling will be opposite). + // To avoid it, we use (float(scrollArea.height) * 0.8) as + // lower bound value of height of page scrolling. (bug 383267) + // XXX shouldn't we use GetPageScrollAmount here? + nscoord pageincrement = nscoord(scrollClampingScrollPort.height - increment); + nscoord pageincrementMin = nscoord(float(scrollClampingScrollPort.height) * 0.8); + FinishReflowForScrollbar(vScroll, minY, maxY, scrollPos.y, + std::max(pageincrement, pageincrementMin), + increment); + } + if (hScroll) { + const double kScrollMultiplier = + Preferences::GetInt("toolkit.scrollbox.horizontalScrollDistance", + NS_DEFAULT_HORIZONTAL_SCROLL_DISTANCE); + nscoord increment = lineScrollAmount.width * kScrollMultiplier; + FinishReflowForScrollbar(hScroll, minX, maxX, scrollPos.x, + nscoord(float(scrollClampingScrollPort.width) * 0.8), + increment); + } + NS_ENSURE_TRUE(weakFrame.IsAlive(), false); + } + + mFrameIsUpdatingScrollbar = false; + // We used to rely on the curpos attribute changes above to scroll the + // view. However, for scrolling to the left of the viewport, we + // rescale the curpos attribute, which means that operations like + // resizing the window while it is scrolled all the way to the left + // hold the curpos attribute constant at 0 while still requiring + // scrolling. So we suppress the effect of the changes above with + // mFrameIsUpdatingScrollbar and call CurPosAttributeChanged here. + // (It actually even works some of the time without this, thanks to + // nsSliderFrame::AttributeChanged's handling of maxpos, but not when + // we hide the scrollbar on a large size change, such as + // maximization.) + if (!mHScrollbarBox && !mVScrollbarBox) + return false; + CurPosAttributeChanged(mVScrollbarBox ? mVScrollbarBox->GetContent() + : mHScrollbarBox->GetContent()); + return true; +} + +void +ScrollFrameHelper::ReflowCallbackCanceled() +{ + mPostedReflowCallback = false; +} + +bool +ScrollFrameHelper::ComputeCustomOverflow(nsOverflowAreas& aOverflowAreas) +{ + nsIScrollableFrame* sf = do_QueryFrame(mOuter); + ScrollbarStyles ss = sf->GetScrollbarStyles(); + + // Reflow when the change in overflow leads to one of our scrollbars + // changing or might require repositioning the scrolled content due to + // reduced extents. + nsRect scrolledRect = GetScrolledRect(); + uint32_t overflowChange = GetOverflowChange(scrolledRect, mPrevScrolledRect); + mPrevScrolledRect = scrolledRect; + + bool needReflow = false; + nsPoint scrollPosition = GetScrollPosition(); + if (overflowChange & nsIScrollableFrame::HORIZONTAL) { + if (ss.mHorizontal != NS_STYLE_OVERFLOW_HIDDEN || scrollPosition.x) { + needReflow = true; + } + } + if (overflowChange & nsIScrollableFrame::VERTICAL) { + if (ss.mVertical != NS_STYLE_OVERFLOW_HIDDEN || scrollPosition.y) { + needReflow = true; + } + } + + if (needReflow) { + // If there are scrollbars, or we're not at the beginning of the pane, + // the scroll position may change. In this case, mark the frame as + // needing reflow. Don't use NS_FRAME_IS_DIRTY as dirty as that means + // we have to reflow the frame and all its descendants, and we don't + // have to do that here. Only this frame needs to be reflowed. + mOuter->PresContext()->PresShell()->FrameNeedsReflow( + mOuter, nsIPresShell::eResize, NS_FRAME_HAS_DIRTY_CHILDREN); + // Ensure that next time nsHTMLScrollFrame::Reflow runs, we don't skip + // updating the scrollbars. (Because the overflow area of the scrolled + // frame has probably just been updated, Reflow won't see it change.) + mSkippedScrollbarLayout = true; + return false; // reflowing will update overflow + } + PostOverflowEvent(); + return mOuter->nsContainerFrame::ComputeCustomOverflow(aOverflowAreas); +} + +void +ScrollFrameHelper::UpdateSticky() +{ + StickyScrollContainer* ssc = StickyScrollContainer:: + GetStickyScrollContainerForScrollFrame(mOuter); + if (ssc) { + nsIScrollableFrame* scrollFrame = do_QueryFrame(mOuter); + ssc->UpdatePositions(scrollFrame->GetScrollPosition(), mOuter); + } +} + +void +ScrollFrameHelper::UpdatePrevScrolledRect() +{ + mPrevScrolledRect = GetScrolledRect(); +} + +void +ScrollFrameHelper::AdjustScrollbarRectForResizer( + nsIFrame* aFrame, nsPresContext* aPresContext, + nsRect& aRect, bool aHasResizer, bool aVertical) +{ + if ((aVertical ? aRect.width : aRect.height) == 0) + return; + + // if a content resizer is present, use its size. Otherwise, check if the + // widget has a resizer. + nsRect resizerRect; + if (aHasResizer) { + resizerRect = mResizerBox->GetRect(); + } + else { + nsPoint offset; + nsIWidget* widget = aFrame->GetNearestWidget(offset); + LayoutDeviceIntRect widgetRect; + if (!widget || !widget->ShowsResizeIndicator(&widgetRect)) + return; + + resizerRect = nsRect(aPresContext->DevPixelsToAppUnits(widgetRect.x) - offset.x, + aPresContext->DevPixelsToAppUnits(widgetRect.y) - offset.y, + aPresContext->DevPixelsToAppUnits(widgetRect.width), + aPresContext->DevPixelsToAppUnits(widgetRect.height)); + } + + if (resizerRect.Contains(aRect.BottomRight() - nsPoint(1, 1))) { + if (aVertical) { + aRect.height = std::max(0, resizerRect.y - aRect.y); + } else { + aRect.width = std::max(0, resizerRect.x - aRect.x); + } + } else if (resizerRect.Contains(aRect.BottomLeft() + nsPoint(1, -1))) { + if (aVertical) { + aRect.height = std::max(0, resizerRect.y - aRect.y); + } else { + nscoord xmost = aRect.XMost(); + aRect.x = std::max(aRect.x, resizerRect.XMost()); + aRect.width = xmost - aRect.x; + } + } +} + +static void +AdjustOverlappingScrollbars(nsRect& aVRect, nsRect& aHRect) +{ + if (aVRect.IsEmpty() || aHRect.IsEmpty()) + return; + + const nsRect oldVRect = aVRect; + const nsRect oldHRect = aHRect; + if (oldVRect.Contains(oldHRect.BottomRight() - nsPoint(1, 1))) { + aHRect.width = std::max(0, oldVRect.x - oldHRect.x); + } else if (oldVRect.Contains(oldHRect.BottomLeft() - nsPoint(0, 1))) { + nscoord overlap = std::min(oldHRect.width, oldVRect.XMost() - oldHRect.x); + aHRect.x += overlap; + aHRect.width -= overlap; + } + if (oldHRect.Contains(oldVRect.BottomRight() - nsPoint(1, 1))) { + aVRect.height = std::max(0, oldHRect.y - oldVRect.y); + } +} + +void +ScrollFrameHelper::LayoutScrollbars(nsBoxLayoutState& aState, + const nsRect& aContentArea, + const nsRect& aOldScrollArea) +{ + NS_ASSERTION(!mSupppressScrollbarUpdate, + "This should have been suppressed"); + + nsIPresShell* presShell = mOuter->PresContext()->PresShell(); + + bool hasResizer = HasResizer(); + bool scrollbarOnLeft = !IsScrollbarOnRight(); + bool overlayScrollBarsWithZoom = + mIsRoot && LookAndFeel::GetInt(LookAndFeel::eIntID_UseOverlayScrollbars) && + presShell->IsScrollPositionClampingScrollPortSizeSet(); + + nsSize scrollPortClampingSize = mScrollPort.Size(); + double res = 1.0; + if (overlayScrollBarsWithZoom) { + scrollPortClampingSize = presShell->GetScrollPositionClampingScrollPortSize(); + res = presShell->GetCumulativeResolution(); + } + + // place the scrollcorner + if (mScrollCornerBox || mResizerBox) { + NS_PRECONDITION(!mScrollCornerBox || mScrollCornerBox->IsXULBoxFrame(), "Must be a box frame!"); + + nsRect r(0, 0, 0, 0); + if (aContentArea.x != mScrollPort.x || scrollbarOnLeft) { + // scrollbar (if any) on left + r.x = aContentArea.x; + r.width = mScrollPort.x - aContentArea.x; + NS_ASSERTION(r.width >= 0, "Scroll area should be inside client rect"); + } else { + // scrollbar (if any) on right + r.width = aContentArea.XMost() - mScrollPort.XMost(); + r.x = aContentArea.XMost() - r.width; + NS_ASSERTION(r.width >= 0, "Scroll area should be inside client rect"); + } + if (aContentArea.y != mScrollPort.y) { + NS_ERROR("top scrollbars not supported"); + } else { + // scrollbar (if any) on bottom + r.height = aContentArea.YMost() - mScrollPort.YMost(); + r.y = aContentArea.YMost() - r.height; + NS_ASSERTION(r.height >= 0, "Scroll area should be inside client rect"); + } + + if (mScrollCornerBox) { + nsBoxFrame::LayoutChildAt(aState, mScrollCornerBox, r); + } + + if (hasResizer) { + // if a resizer is present, get its size. Assume a default size of 15 pixels. + nscoord defaultSize = nsPresContext::CSSPixelsToAppUnits(15); + nsSize resizerMinSize = mResizerBox->GetXULMinSize(aState); + + nscoord vScrollbarWidth = mVScrollbarBox ? + mVScrollbarBox->GetXULPrefSize(aState).width : defaultSize; + r.width = std::max(std::max(r.width, vScrollbarWidth), resizerMinSize.width); + if (aContentArea.x == mScrollPort.x && !scrollbarOnLeft) { + r.x = aContentArea.XMost() - r.width; + } + + nscoord hScrollbarHeight = mHScrollbarBox ? + mHScrollbarBox->GetXULPrefSize(aState).height : defaultSize; + r.height = std::max(std::max(r.height, hScrollbarHeight), resizerMinSize.height); + if (aContentArea.y == mScrollPort.y) { + r.y = aContentArea.YMost() - r.height; + } + + nsBoxFrame::LayoutChildAt(aState, mResizerBox, r); + } + else if (mResizerBox) { + // otherwise lay out the resizer with an empty rectangle + nsBoxFrame::LayoutChildAt(aState, mResizerBox, nsRect()); + } + } + + nsPresContext* presContext = mScrolledFrame->PresContext(); + nsRect vRect; + if (mVScrollbarBox) { + NS_PRECONDITION(mVScrollbarBox->IsXULBoxFrame(), "Must be a box frame!"); + vRect = mScrollPort; + if (overlayScrollBarsWithZoom) { + vRect.height = NSToCoordRound(res * scrollPortClampingSize.height); + } + vRect.width = aContentArea.width - mScrollPort.width; + vRect.x = scrollbarOnLeft ? aContentArea.x : mScrollPort.x + NSToCoordRound(res * scrollPortClampingSize.width); + if (mHasVerticalScrollbar) { + nsMargin margin; + mVScrollbarBox->GetXULMargin(margin); + vRect.Deflate(margin); + } + AdjustScrollbarRectForResizer(mOuter, presContext, vRect, hasResizer, true); + } + + nsRect hRect; + if (mHScrollbarBox) { + NS_PRECONDITION(mHScrollbarBox->IsXULBoxFrame(), "Must be a box frame!"); + hRect = mScrollPort; + if (overlayScrollBarsWithZoom) { + hRect.width = NSToCoordRound(res * scrollPortClampingSize.width); + } + hRect.height = aContentArea.height - mScrollPort.height; + hRect.y = mScrollPort.y + NSToCoordRound(res * scrollPortClampingSize.height); + if (mHasHorizontalScrollbar) { + nsMargin margin; + mHScrollbarBox->GetXULMargin(margin); + hRect.Deflate(margin); + } + AdjustScrollbarRectForResizer(mOuter, presContext, hRect, hasResizer, false); + } + + if (!LookAndFeel::GetInt(LookAndFeel::eIntID_AllowOverlayScrollbarsOverlap)) { + AdjustOverlappingScrollbars(vRect, hRect); + } + if (mVScrollbarBox) { + nsBoxFrame::LayoutChildAt(aState, mVScrollbarBox, vRect); + } + if (mHScrollbarBox) { + nsBoxFrame::LayoutChildAt(aState, mHScrollbarBox, hRect); + } + + // may need to update fixed position children of the viewport, + // if the client area changed size because of an incremental + // reflow of a descendant. (If the outer frame is dirty, the fixed + // children will be re-laid out anyway) + if (aOldScrollArea.Size() != mScrollPort.Size() && + !(mOuter->GetStateBits() & NS_FRAME_IS_DIRTY) && + mIsRoot) { + mMayHaveDirtyFixedChildren = true; + } + + // post reflow callback to modify scrollbar attributes + mUpdateScrollbarAttributes = true; + if (!mPostedReflowCallback) { + aState.PresContext()->PresShell()->PostReflowCallback(this); + mPostedReflowCallback = true; + } +} + +#if DEBUG +static bool ShellIsAlive(nsWeakPtr& aWeakPtr) +{ + nsCOMPtr<nsIPresShell> shell(do_QueryReferent(aWeakPtr)); + return !!shell; +} +#endif + +void +ScrollFrameHelper::SetScrollbarEnabled(nsIContent* aContent, nscoord aMaxPos) +{ + DebugOnly<nsWeakPtr> weakShell( + do_GetWeakReference(mOuter->PresContext()->PresShell())); + if (aMaxPos) { + aContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); + } else { + aContent->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, + NS_LITERAL_STRING("true"), true); + } + MOZ_ASSERT(ShellIsAlive(weakShell), "pres shell was destroyed by scrolling"); +} + +void +ScrollFrameHelper::SetCoordAttribute(nsIContent* aContent, nsIAtom* aAtom, + nscoord aSize) +{ + DebugOnly<nsWeakPtr> weakShell( + do_GetWeakReference(mOuter->PresContext()->PresShell())); + // convert to pixels + int32_t pixelSize = nsPresContext::AppUnitsToIntCSSPixels(aSize); + + // only set the attribute if it changed. + + nsAutoString newValue; + newValue.AppendInt(pixelSize); + + if (aContent->AttrValueIs(kNameSpaceID_None, aAtom, newValue, eCaseMatters)) + return; + + nsWeakFrame weakFrame(mOuter); + nsCOMPtr<nsIContent> kungFuDeathGrip = aContent; + aContent->SetAttr(kNameSpaceID_None, aAtom, newValue, true); + MOZ_ASSERT(ShellIsAlive(weakShell), "pres shell was destroyed by scrolling"); + if (!weakFrame.IsAlive()) { + return; + } + + if (mScrollbarActivity) { + RefPtr<ScrollbarActivity> scrollbarActivity(mScrollbarActivity); + scrollbarActivity->ActivityOccurred(); + } +} + +static void +ReduceRadii(nscoord aXBorder, nscoord aYBorder, + nscoord& aXRadius, nscoord& aYRadius) +{ + // In order to ensure that the inside edge of the border has no + // curvature, we need at least one of its radii to be zero. + if (aXRadius <= aXBorder || aYRadius <= aYBorder) + return; + + // For any corner where we reduce the radii, preserve the corner's shape. + double ratio = std::max(double(aXBorder) / aXRadius, + double(aYBorder) / aYRadius); + aXRadius *= ratio; + aYRadius *= ratio; +} + +/** + * Implement an override for nsIFrame::GetBorderRadii to ensure that + * the clipping region for the border radius does not clip the scrollbars. + * + * In other words, we require that the border radius be reduced until the + * inner border radius at the inner edge of the border is 0 wherever we + * have scrollbars. + */ +bool +ScrollFrameHelper::GetBorderRadii(const nsSize& aFrameSize, + const nsSize& aBorderArea, + Sides aSkipSides, + nscoord aRadii[8]) const +{ + if (!mOuter->nsContainerFrame::GetBorderRadii(aFrameSize, aBorderArea, + aSkipSides, aRadii)) { + return false; + } + + // Since we can use GetActualScrollbarSizes (rather than + // GetDesiredScrollbarSizes) since this doesn't affect reflow, we + // probably should. + nsMargin sb = GetActualScrollbarSizes(); + nsMargin border = mOuter->GetUsedBorder(); + + if (sb.left > 0 || sb.top > 0) { + ReduceRadii(border.left, border.top, + aRadii[NS_CORNER_TOP_LEFT_X], + aRadii[NS_CORNER_TOP_LEFT_Y]); + } + + if (sb.top > 0 || sb.right > 0) { + ReduceRadii(border.right, border.top, + aRadii[NS_CORNER_TOP_RIGHT_X], + aRadii[NS_CORNER_TOP_RIGHT_Y]); + } + + if (sb.right > 0 || sb.bottom > 0) { + ReduceRadii(border.right, border.bottom, + aRadii[NS_CORNER_BOTTOM_RIGHT_X], + aRadii[NS_CORNER_BOTTOM_RIGHT_Y]); + } + + if (sb.bottom > 0 || sb.left > 0) { + ReduceRadii(border.left, border.bottom, + aRadii[NS_CORNER_BOTTOM_LEFT_X], + aRadii[NS_CORNER_BOTTOM_LEFT_Y]); + } + + return true; +} + +static nscoord +SnapCoord(nscoord aCoord, double aRes, nscoord aAppUnitsPerPixel) +{ + double snappedToLayerPixels = NS_round((aRes*aCoord)/aAppUnitsPerPixel); + return NSToCoordRoundWithClamp(snappedToLayerPixels*aAppUnitsPerPixel/aRes); +} + +nsRect +ScrollFrameHelper::GetScrolledRect() const +{ + nsRect result = + GetUnsnappedScrolledRectInternal(mScrolledFrame->GetScrollableOverflowRect(), + mScrollPort.Size()); + + if (result.width < mScrollPort.width) { + NS_WARNING("Scrolled rect smaller than scrollport?"); + } + if (result.height < mScrollPort.height) { + NS_WARNING("Scrolled rect smaller than scrollport?"); + } + + // Expand / contract the result by up to half a layer pixel so that scrolling + // to the right / bottom edge does not change the layer pixel alignment of + // the scrolled contents. + + if (result.x == 0 && result.y == 0 && + result.width == mScrollPort.width && + result.height == mScrollPort.height) { + // The edges that we would snap are already aligned with the scroll port, + // so we can skip all the work below. + return result; + } + + // For that, we first convert the scroll port and the scrolled rect to rects + // relative to the reference frame, since that's the space where painting does + // snapping. + nsSize scrollPortSize = GetScrollPositionClampingScrollPortSize(); + nsIFrame* referenceFrame = nsLayoutUtils::GetReferenceFrame(mOuter); + nsPoint toReferenceFrame = mOuter->GetOffsetToCrossDoc(referenceFrame); + nsRect scrollPort(mScrollPort.TopLeft() + toReferenceFrame, scrollPortSize); + nsRect scrolledRect = result + scrollPort.TopLeft(); + + if (scrollPort.Overflows() || scrolledRect.Overflows()) { + return result; + } + + // Now, snap the bottom right corner of both of these rects. + // We snap to layer pixels, so we need to respect the layer's scale. + nscoord appUnitsPerDevPixel = mScrolledFrame->PresContext()->AppUnitsPerDevPixel(); + gfxSize scale = FrameLayerBuilder::GetPaintedLayerScaleForFrame(mScrolledFrame); + if (scale.IsEmpty()) { + scale = gfxSize(1.0f, 1.0f); + } + + // Compute bounds for the scroll position, and computed the snapped scrolled + // rect from the scroll position bounds. + nscoord snappedScrolledAreaBottom = SnapCoord(scrolledRect.YMost(), scale.height, appUnitsPerDevPixel); + nscoord snappedScrollPortBottom = SnapCoord(scrollPort.YMost(), scale.height, appUnitsPerDevPixel); + nscoord maximumScrollOffsetY = snappedScrolledAreaBottom - snappedScrollPortBottom; + result.SetBottomEdge(scrollPort.height + maximumScrollOffsetY); + + if (GetScrolledFrameDir() == NS_STYLE_DIRECTION_LTR) { + nscoord snappedScrolledAreaRight = SnapCoord(scrolledRect.XMost(), scale.width, appUnitsPerDevPixel); + nscoord snappedScrollPortRight = SnapCoord(scrollPort.XMost(), scale.width, appUnitsPerDevPixel); + nscoord maximumScrollOffsetX = snappedScrolledAreaRight - snappedScrollPortRight; + result.SetRightEdge(scrollPort.width + maximumScrollOffsetX); + } else { + // In RTL, the scrolled area's right edge is at scrollPort.XMost(), + // and the scrolled area's x position is zero or negative. We want + // the right edge to stay flush with the scroll port, so we snap the + // left edge. + nscoord snappedScrolledAreaLeft = SnapCoord(scrolledRect.x, scale.width, appUnitsPerDevPixel); + nscoord snappedScrollPortLeft = SnapCoord(scrollPort.x, scale.width, appUnitsPerDevPixel); + nscoord minimumScrollOffsetX = snappedScrolledAreaLeft - snappedScrollPortLeft; + result.SetLeftEdge(minimumScrollOffsetX); + } + + return result; +} + + +uint8_t +ScrollFrameHelper::GetScrolledFrameDir() const +{ + // If the scrolled frame has unicode-bidi: plaintext, the paragraph + // direction set by the text content overrides the direction of the frame + if (mScrolledFrame->StyleTextReset()->mUnicodeBidi & + NS_STYLE_UNICODE_BIDI_PLAINTEXT) { + nsIFrame* childFrame = mScrolledFrame->PrincipalChildList().FirstChild(); + if (childFrame) { + return (nsBidiPresUtils::ParagraphDirection(childFrame) == NSBIDI_LTR) + ? NS_STYLE_DIRECTION_LTR : NS_STYLE_DIRECTION_RTL; + } + } + + return IsBidiLTR() ? NS_STYLE_DIRECTION_LTR : NS_STYLE_DIRECTION_RTL; +} + +nsRect +ScrollFrameHelper::GetUnsnappedScrolledRectInternal(const nsRect& aScrolledFrameOverflowArea, + const nsSize& aScrollPortSize) const +{ + return nsLayoutUtils::GetScrolledRect(mScrolledFrame, + aScrolledFrameOverflowArea, + aScrollPortSize, GetScrolledFrameDir()); +} + +nsMargin +ScrollFrameHelper::GetActualScrollbarSizes() const +{ + nsRect r = mOuter->GetPaddingRect() - mOuter->GetPosition(); + + return nsMargin(mScrollPort.y - r.y, + r.XMost() - mScrollPort.XMost(), + r.YMost() - mScrollPort.YMost(), + mScrollPort.x - r.x); +} + +void +ScrollFrameHelper::SetScrollbarVisibility(nsIFrame* aScrollbar, bool aVisible) +{ + nsScrollbarFrame* scrollbar = do_QueryFrame(aScrollbar); + if (scrollbar) { + // See if we have a mediator. + nsIScrollbarMediator* mediator = scrollbar->GetScrollbarMediator(); + if (mediator) { + // Inform the mediator of the visibility change. + mediator->VisibilityChanged(aVisible); + } + } +} + +nscoord +ScrollFrameHelper::GetCoordAttribute(nsIFrame* aBox, nsIAtom* aAtom, + nscoord aDefaultValue, + nscoord* aRangeStart, + nscoord* aRangeLength) +{ + if (aBox) { + nsIContent* content = aBox->GetContent(); + + nsAutoString value; + content->GetAttr(kNameSpaceID_None, aAtom, value); + if (!value.IsEmpty()) + { + nsresult error; + // convert it to appunits + nscoord result = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error)); + nscoord halfPixel = nsPresContext::CSSPixelsToAppUnits(0.5f); + // Any nscoord value that would round to the attribute value when converted + // to CSS pixels is allowed. + *aRangeStart = result - halfPixel; + *aRangeLength = halfPixel*2 - 1; + return result; + } + } + + // Only this exact default value is allowed. + *aRangeStart = aDefaultValue; + *aRangeLength = 0; + return aDefaultValue; +} + +nsPresState* +ScrollFrameHelper::SaveState() const +{ + nsIScrollbarMediator* mediator = do_QueryFrame(GetScrolledFrame()); + if (mediator) { + // child handles its own scroll state, so don't bother saving state here + return nullptr; + } + + // Don't store a scroll state if we never have been scrolled or restored + // a previous scroll state, and we're not in the middle of a smooth scroll. + bool isInSmoothScroll = IsProcessingAsyncScroll() || mLastSmoothScrollOrigin; + if (!mHasBeenScrolled && !mDidHistoryRestore && !isInSmoothScroll) { + return nullptr; + } + + nsPresState* state = new nsPresState(); + bool allowScrollOriginDowngrade = + !nsLayoutUtils::CanScrollOriginClobberApz(mLastScrollOrigin) || + mAllowScrollOriginDowngrade; + // Save mRestorePos instead of our actual current scroll position, if it's + // valid and we haven't moved since the last update of mLastPos (same check + // that ScrollToRestoredPosition uses). This ensures if a reframe occurs + // while we're in the process of loading content to scroll to a restored + // position, we'll keep trying after the reframe. Similarly, if we're in the + // middle of a smooth scroll, store the destination so that when we restore + // we'll jump straight to the end of the scroll animation, rather than + // effectively dropping it. Note that the mRestorePos will override the + // smooth scroll destination if both are present. + nsPoint pt = GetLogicalScrollPosition(); + if (isInSmoothScroll) { + pt = mDestination; + allowScrollOriginDowngrade = false; + } + if (mRestorePos.y != -1 && pt == mLastPos) { + pt = mRestorePos; + } + state->SetScrollState(pt); + state->SetAllowScrollOriginDowngrade(allowScrollOriginDowngrade); + if (mIsRoot) { + // Only save resolution properties for root scroll frames + nsIPresShell* shell = mOuter->PresContext()->PresShell(); + state->SetResolution(shell->GetResolution()); + state->SetScaleToResolution(shell->ScaleToResolution()); + } + return state; +} + +void +ScrollFrameHelper::RestoreState(nsPresState* aState) +{ + mRestorePos = aState->GetScrollPosition(); + MOZ_ASSERT(mLastScrollOrigin == nsGkAtoms::other); + mAllowScrollOriginDowngrade = aState->GetAllowScrollOriginDowngrade(); + mDidHistoryRestore = true; + mLastPos = mScrolledFrame ? GetLogicalScrollPosition() : nsPoint(0,0); + + // Resolution properties should only exist on root scroll frames. + MOZ_ASSERT(mIsRoot || (!aState->GetScaleToResolution() && + aState->GetResolution() == 1.0)); + + if (mIsRoot) { + nsIPresShell* presShell = mOuter->PresContext()->PresShell(); + if (aState->GetScaleToResolution()) { + presShell->SetResolutionAndScaleTo(aState->GetResolution()); + } else { + presShell->SetResolution(aState->GetResolution()); + } + } +} + +void +ScrollFrameHelper::PostScrolledAreaEvent() +{ + if (mScrolledAreaEvent.IsPending()) { + return; + } + mScrolledAreaEvent = new ScrolledAreaEvent(this); + nsContentUtils::AddScriptRunner(mScrolledAreaEvent.get()); +} + +//////////////////////////////////////////////////////////////////////////////// +// ScrolledArea change event dispatch + +NS_IMETHODIMP +ScrollFrameHelper::ScrolledAreaEvent::Run() +{ + if (mHelper) { + mHelper->FireScrolledAreaEvent(); + } + return NS_OK; +} + +void +ScrollFrameHelper::FireScrolledAreaEvent() +{ + mScrolledAreaEvent.Forget(); + + InternalScrollAreaEvent event(true, eScrolledAreaChanged, nullptr); + nsPresContext *prescontext = mOuter->PresContext(); + nsIContent* content = mOuter->GetContent(); + + event.mArea = mScrolledFrame->GetScrollableOverflowRectRelativeToParent(); + + nsIDocument *doc = content->GetUncomposedDoc(); + if (doc) { + EventDispatcher::Dispatch(doc, prescontext, &event, nullptr); + } +} + +uint32_t +nsIScrollableFrame::GetPerceivedScrollingDirections() const +{ + nscoord oneDevPixel = GetScrolledFrame()->PresContext()->AppUnitsPerDevPixel(); + uint32_t directions = GetScrollbarVisibility(); + nsRect scrollRange = GetScrollRange(); + if (scrollRange.width >= oneDevPixel) { + directions |= HORIZONTAL; + } + if (scrollRange.height >= oneDevPixel) { + directions |= VERTICAL; + } + return directions; +} + +/** + * Collect the scroll-snap-coordinates of frames in the subtree rooted at + * |aFrame|, relative to |aScrolledFrame|, into |aOutCoords|. + */ +void +CollectScrollSnapCoordinates(nsIFrame* aFrame, nsIFrame* aScrolledFrame, + nsTArray<nsPoint>& aOutCoords) +{ + nsIFrame::ChildListIterator childLists(aFrame); + for (; !childLists.IsDone(); childLists.Next()) { + nsFrameList::Enumerator childFrames(childLists.CurrentList()); + for (; !childFrames.AtEnd(); childFrames.Next()) { + nsIFrame* f = childFrames.get(); + + const nsStyleDisplay* styleDisplay = f->StyleDisplay(); + size_t coordCount = styleDisplay->mScrollSnapCoordinate.Length(); + + if (coordCount) { + nsRect frameRect = f->GetRect(); + nsPoint offset = f->GetOffsetTo(aScrolledFrame); + nsRect edgesRect = nsRect(offset, frameRect.Size()); + for (size_t coordNum = 0; coordNum < coordCount; coordNum++) { + const Position& coordPosition = + f->StyleDisplay()->mScrollSnapCoordinate[coordNum]; + nsPoint coordPoint = edgesRect.TopLeft(); + coordPoint += nsPoint(coordPosition.mXPosition.mLength, + coordPosition.mYPosition.mLength); + if (coordPosition.mXPosition.mHasPercent) { + coordPoint.x += NSToCoordRound(coordPosition.mXPosition.mPercent * + frameRect.width); + } + if (coordPosition.mYPosition.mHasPercent) { + coordPoint.y += NSToCoordRound(coordPosition.mYPosition.mPercent * + frameRect.height); + } + + aOutCoords.AppendElement(coordPoint); + } + } + + CollectScrollSnapCoordinates(f, aScrolledFrame, aOutCoords); + } + } +} + +layers::ScrollSnapInfo +ComputeScrollSnapInfo(const ScrollFrameHelper& aScrollFrame) +{ + ScrollSnapInfo result; + + ScrollbarStyles styles = aScrollFrame.GetScrollbarStylesFromFrame(); + + if (styles.mScrollSnapTypeY == NS_STYLE_SCROLL_SNAP_TYPE_NONE && + styles.mScrollSnapTypeX == NS_STYLE_SCROLL_SNAP_TYPE_NONE) { + // We won't be snapping, short-circuit the computation. + return result; + } + + result.mScrollSnapTypeX = styles.mScrollSnapTypeX; + result.mScrollSnapTypeY = styles.mScrollSnapTypeY; + + nsSize scrollPortSize = aScrollFrame.GetScrollPortRect().Size(); + + result.mScrollSnapDestination = nsPoint(styles.mScrollSnapDestinationX.mLength, + styles.mScrollSnapDestinationY.mLength); + if (styles.mScrollSnapDestinationX.mHasPercent) { + result.mScrollSnapDestination.x += + NSToCoordFloorClamped(styles.mScrollSnapDestinationX.mPercent * + scrollPortSize.width); + } + if (styles.mScrollSnapDestinationY.mHasPercent) { + result.mScrollSnapDestination.y += + NSToCoordFloorClamped(styles.mScrollSnapDestinationY.mPercent * + scrollPortSize.height); + } + + if (styles.mScrollSnapPointsX.GetUnit() != eStyleUnit_None) { + result.mScrollSnapIntervalX = Some(nsRuleNode::ComputeCoordPercentCalc( + styles.mScrollSnapPointsX, scrollPortSize.width)); + } + if (styles.mScrollSnapPointsY.GetUnit() != eStyleUnit_None) { + result.mScrollSnapIntervalY = Some(nsRuleNode::ComputeCoordPercentCalc( + styles.mScrollSnapPointsY, scrollPortSize.height)); + } + + CollectScrollSnapCoordinates(aScrollFrame.GetScrolledFrame(), + aScrollFrame.GetScrolledFrame(), + result.mScrollSnapCoordinates); + + return result; +} + +layers::ScrollSnapInfo +ScrollFrameHelper::GetScrollSnapInfo() const +{ + // TODO(botond): Should we cache it? + return ComputeScrollSnapInfo(*this); +} + +bool +ScrollFrameHelper::GetSnapPointForDestination(nsIScrollableFrame::ScrollUnit aUnit, + nsPoint aStartPos, + nsPoint &aDestination) +{ + Maybe<nsPoint> snapPoint = ScrollSnapUtils::GetSnapPointForDestination( + GetScrollSnapInfo(), aUnit, mScrollPort.Size(), + GetScrollRangeForClamping(), aStartPos, aDestination); + if (snapPoint) { + aDestination = snapPoint.ref(); + return true; + } + return false; +} + +bool +ScrollFrameHelper::UsesContainerScrolling() const +{ + if (gfxPrefs::LayoutUseContainersForRootFrames()) { + return mIsRoot; + } + return false; +} |