/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* rendering object to wrap rendering objects that should be scrollable */ #include "nsGfxScrollFrame.h" #include "ScrollPositionUpdate.h" #include "mozilla/layers/LayersTypes.h" #include "nsIXULRuntime.h" #include "base/compiler_specific.h" #include "DisplayItemClip.h" #include "nsCOMPtr.h" #include "nsIDocumentViewer.h" #include "nsPresContext.h" #include "nsView.h" #include "nsViewportInfo.h" #include "nsContainerFrame.h" #include "nsGkAtoms.h" #include "nsNameSpaceManager.h" #include "mozilla/intl/BidiEmbeddingLevel.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/gfx/gfxVars.h" #include "nsFontMetrics.h" #include "mozilla/dom/NodeInfo.h" #include "nsScrollbarFrame.h" #include "nsINode.h" #include "nsIScrollbarMediator.h" #include "nsITextControlFrame.h" #include "nsILayoutHistoryState.h" #include "nsNodeInfoManager.h" #include "nsContentCreatorFunctions.h" #include "nsStyleTransformMatrix.h" #include "mozilla/PresState.h" #include "nsContentUtils.h" #include "nsDisplayList.h" #include "nsHTMLDocument.h" #include "nsLayoutUtils.h" #include "nsBidiPresUtils.h" #include "nsBidiUtils.h" #include "nsDocShell.h" #include "mozilla/ContentEvents.h" #include "mozilla/DisplayPortUtils.h" #include "mozilla/EventDispatcher.h" #include "mozilla/Preferences.h" #include "mozilla/PresShell.h" #include "mozilla/ScopeExit.h" #include "mozilla/ScrollbarPreferences.h" #include "mozilla/ScrollingMetrics.h" #include "mozilla/StaticPrefs_bidi.h" #include "mozilla/StaticPrefs_browser.h" #include "mozilla/StaticPrefs_toolkit.h" #include "mozilla/StaticPtr.h" #include "mozilla/SVGOuterSVGFrame.h" #include "mozilla/ViewportUtils.h" #include "mozilla/LookAndFeel.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/HTMLMarqueeElement.h" #include "mozilla/dom/ScrollTimeline.h" #include "mozilla/dom/BrowserChild.h" #include #include "mozilla/MathAlgorithms.h" #include "mozilla/Telemetry.h" #include "nsSubDocumentFrame.h" #include "mozilla/Attributes.h" #include "ScrollbarActivity.h" #include "nsRefreshDriver.h" #include "nsStyleConsts.h" #include "nsIScrollPositionListener.h" #include "StickyScrollContainer.h" #include "nsIFrameInlines.h" #include "gfxPlatform.h" #include "mozilla/StaticPrefs_apz.h" #include "mozilla/StaticPrefs_general.h" #include "mozilla/StaticPrefs_layers.h" #include "mozilla/StaticPrefs_layout.h" #include "mozilla/StaticPrefs_mousewheel.h" #include "mozilla/ToString.h" #include "ScrollAnimationPhysics.h" #include "ScrollAnimationBezierPhysics.h" #include "ScrollAnimationMSDPhysics.h" #include "ScrollSnap.h" #include "UnitTransforms.h" #include "nsSliderFrame.h" #include "ViewportFrame.h" #include "mozilla/gfx/gfxVars.h" #include "mozilla/layers/APZCCallbackHelper.h" #include "mozilla/layers/APZPublicUtils.h" #include "mozilla/layers/AxisPhysicsModel.h" #include "mozilla/layers/AxisPhysicsMSDModel.h" #include "mozilla/layers/ScrollingInteractionContext.h" #include "mozilla/layers/ScrollLinkedEffectDetector.h" #include "mozilla/Unused.h" #include "MobileViewportManager.h" #include "VisualViewport.h" #include "WindowRenderer.h" #include #include // for std::abs(int/long) #include // for std::abs(float/double) #include // for std::tie static mozilla::LazyLogModule sApzPaintSkipLog("apz.paintskip"); #define PAINT_SKIP_LOG(...) \ MOZ_LOG(sApzPaintSkipLog, LogLevel::Debug, (__VA_ARGS__)) static mozilla::LazyLogModule sScrollRestoreLog("scrollrestore"); #define SCROLLRESTORE_LOG(...) \ MOZ_LOG(sScrollRestoreLog, LogLevel::Debug, (__VA_ARGS__)) static mozilla::LazyLogModule sRootScrollbarsLog("rootscrollbars"); #define ROOT_SCROLLBAR_LOG(...) \ if (mIsRoot) { \ MOZ_LOG(sRootScrollbarsLog, LogLevel::Debug, (__VA_ARGS__)); \ } static mozilla::LazyLogModule sDisplayportLog("apz.displayport"); using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::gfx; using namespace mozilla::layers; using namespace mozilla::layout; using nsStyleTransformMatrix::TransformReferenceBox; static ScrollDirections GetOverflowChange(const nsRect& aCurScrolledRect, const nsRect& aPrevScrolledRect) { ScrollDirections result; if (aPrevScrolledRect.x != aCurScrolledRect.x || aPrevScrolledRect.width != aCurScrolledRect.width) { result += ScrollDirection::eHorizontal; } if (aPrevScrolledRect.y != aCurScrolledRect.y || aPrevScrolledRect.height != aCurScrolledRect.height) { result += ScrollDirection::eVertical; } return result; } /** * This class handles the dispatching of scroll events to content. * * Scroll events are posted to the refresh driver via * nsRefreshDriver::PostScrollEvent(), and they are fired during a refresh * driver tick, after running requestAnimationFrame callbacks but before * the style flush. This allows rAF callbacks to perform scrolling and have * that scrolling be reflected on the same refresh driver tick, while at * the same time allowing scroll event listeners to make style changes and * have those style changes be reflected on the same refresh driver tick. * * ScrollEvents cannot be refresh observers, because none of the existing * categories of refresh observers (FlushType::Style, FlushType::Layout, * and FlushType::Display) are run at the desired time in a refresh driver * tick. They behave similarly to refresh observers in that their presence * causes the refresh driver to tick. * * ScrollEvents are one-shot runnables; the refresh driver drops them after * running them. */ class nsHTMLScrollFrame::ScrollEvent : public Runnable { public: NS_DECL_NSIRUNNABLE explicit ScrollEvent(nsHTMLScrollFrame* aHelper, bool aDelayed); void Revoke() { mHelper = nullptr; } private: nsHTMLScrollFrame* mHelper; }; class nsHTMLScrollFrame::ScrollEndEvent : public Runnable { public: NS_DECL_NSIRUNNABLE explicit ScrollEndEvent(nsHTMLScrollFrame* aHelper); void Revoke() { mHelper = nullptr; } private: nsHTMLScrollFrame* mHelper; }; class nsHTMLScrollFrame::AsyncScrollPortEvent : public Runnable { public: NS_DECL_NSIRUNNABLE explicit AsyncScrollPortEvent(nsHTMLScrollFrame* helper) : Runnable("nsHTMLScrollFrame::AsyncScrollPortEvent"), mHelper(helper) {} void Revoke() { mHelper = nullptr; } private: nsHTMLScrollFrame* mHelper; }; class nsHTMLScrollFrame::ScrolledAreaEvent : public Runnable { public: NS_DECL_NSIRUNNABLE explicit ScrolledAreaEvent(nsHTMLScrollFrame* helper) : Runnable("nsHTMLScrollFrame::ScrolledAreaEvent"), mHelper(helper) {} void Revoke() { mHelper = nullptr; } private: nsHTMLScrollFrame* mHelper; }; //---------------------------------------------------------------------- //----------nsHTMLScrollFrame------------------------------------------- class ScrollFrameActivityTracker final : public nsExpirationTracker { public: // Wait for 3-4s between scrolls before we remove our layers. // That's 4 generations of 1s each. enum { TIMEOUT_MS = 1000 }; explicit ScrollFrameActivityTracker(nsIEventTarget* aEventTarget) : nsExpirationTracker( TIMEOUT_MS, "ScrollFrameActivityTracker", aEventTarget) {} ~ScrollFrameActivityTracker() { AgeAllGenerations(); } virtual void NotifyExpired(nsHTMLScrollFrame* aObject) override { RemoveObject(aObject); aObject->MarkNotRecentlyScrolled(); } }; static StaticAutoPtr gScrollFrameActivityTracker; nsHTMLScrollFrame* NS_NewHTMLScrollFrame(PresShell* aPresShell, ComputedStyle* aStyle, bool aIsRoot) { return new (aPresShell) nsHTMLScrollFrame(aStyle, aPresShell->GetPresContext(), aIsRoot); } NS_IMPL_FRAMEARENA_HELPERS(nsHTMLScrollFrame) nsHTMLScrollFrame::nsHTMLScrollFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, nsIFrame::ClassID aID, bool aIsRoot) : nsContainerFrame(aStyle, aPresContext, aID), mHScrollbarBox(nullptr), mVScrollbarBox(nullptr), mScrolledFrame(nullptr), mScrollCornerBox(nullptr), mResizerBox(nullptr), mReferenceFrameDuringPainting(nullptr), mAsyncScroll(nullptr), mAsyncSmoothMSDScroll(nullptr), mLastScrollOrigin(ScrollOrigin::None), mDestination(0, 0), mRestorePos(-1, -1), mLastPos(-1, -1), mApzScrollPos(0, 0), mLastUpdateFramesPos(-1, -1), mScrollParentID(mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID), mAnchor(this), mCurrentAPZScrollAnimationType(APZScrollAnimationType::No), mIsFirstScrollableFrameSequenceNumber(Nothing()), mInScrollingGesture(InScrollingGesture::No), mAllowScrollOriginDowngrade(false), mHadDisplayPortAtLastFrameUpdate(false), mHasVerticalScrollbar(false), mHasHorizontalScrollbar(false), mOnlyNeedVScrollbarToScrollVVInsideLV(false), mOnlyNeedHScrollbarToScrollVVInsideLV(false), mFrameIsUpdatingScrollbar(false), mDidHistoryRestore(false), mIsRoot(aIsRoot), mSuppressScrollbarUpdate(false), mSkippedScrollbarLayout(false), mHadNonInitialReflow(false), mFirstReflow(true), mHorizontalOverflow(false), mVerticalOverflow(false), mPostedReflowCallback(false), mMayHaveDirtyFixedChildren(false), mUpdateScrollbarAttributes(false), mHasBeenScrolledRecently(false), mWillBuildScrollableLayer(false), mIsParentToActiveScrollFrames(false), mHasBeenScrolled(false), mIgnoreMomentumScroll(false), mTransformingByAPZ(false), mScrollableByAPZ(false), mZoomableByAPZ(false), mHasOutOfFlowContentInsideFilter(false), mSuppressScrollbarRepaints(false), mIsUsingMinimumScaleSize(false), mMinimumScaleSizeChanged(false), mProcessingScrollEvent(false), mApzAnimationRequested(false), mApzAnimationTriggeredByScriptRequested(false), mReclampVVOffsetInReflowFinished(false), mMayScheduleScrollAnimations(false), #ifdef MOZ_WIDGET_ANDROID mHasVerticalOverflowForDynamicToolbar(false), #endif mVelocityQueue(PresContext()) { AppendScrollUpdate(ScrollPositionUpdate::NewScrollframe(nsPoint())); if (UsesOverlayScrollbars()) { mScrollbarActivity = new ScrollbarActivity(this); } if (mIsRoot) { mZoomableByAPZ = PresShell()->GetZoomableByAPZ(); } } nsHTMLScrollFrame::~nsHTMLScrollFrame() = default; void nsHTMLScrollFrame::ScrollbarActivityStarted() const { if (mScrollbarActivity) { mScrollbarActivity->ActivityStarted(); } } void nsHTMLScrollFrame::ScrollbarActivityStopped() const { if (mScrollbarActivity) { mScrollbarActivity->ActivityStopped(); } } void nsHTMLScrollFrame::Destroy(DestroyContext& aContext) { DestroyAbsoluteFrames(aContext); if (mIsRoot) { PresShell()->ResetVisualViewportOffset(); } mAnchor.Destroy(); if (mScrollbarActivity) { mScrollbarActivity->Destroy(); mScrollbarActivity = nullptr; } // Unbind the content created in CreateAnonymousContent later... aContext.AddAnonymousContent(mHScrollbarContent.forget()); aContext.AddAnonymousContent(mVScrollbarContent.forget()); aContext.AddAnonymousContent(mScrollCornerContent.forget()); aContext.AddAnonymousContent(mResizerContent.forget()); if (mPostedReflowCallback) { PresShell()->CancelReflowCallback(this); mPostedReflowCallback = false; } if (mDisplayPortExpiryTimer) { mDisplayPortExpiryTimer->Cancel(); mDisplayPortExpiryTimer = nullptr; } if (mActivityExpirationState.IsTracked()) { gScrollFrameActivityTracker->RemoveObject(this); } if (gScrollFrameActivityTracker && gScrollFrameActivityTracker->IsEmpty()) { gScrollFrameActivityTracker = nullptr; } if (mScrollActivityTimer) { mScrollActivityTimer->Cancel(); mScrollActivityTimer = nullptr; } RemoveObservers(); if (mScrollEvent) { mScrollEvent->Revoke(); } if (mScrollEndEvent) { mScrollEndEvent->Revoke(); } nsContainerFrame::Destroy(aContext); } void nsHTMLScrollFrame::SetInitialChildList(ChildListID aListID, nsFrameList&& aChildList) { nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList)); ReloadChildFrames(); } void nsHTMLScrollFrame::AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) { NS_ASSERTION(aListID == FrameChildListID::Principal, "Only main list supported"); mFrames.AppendFrames(nullptr, std::move(aFrameList)); ReloadChildFrames(); } void nsHTMLScrollFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, const nsLineList::iterator* aPrevFrameLine, nsFrameList&& aFrameList) { NS_ASSERTION(aListID == FrameChildListID::Principal, "Only main list supported"); NS_ASSERTION(!aPrevFrame || aPrevFrame->GetParent() == this, "inserting after sibling frame with different parent"); mFrames.InsertFrames(nullptr, aPrevFrame, std::move(aFrameList)); ReloadChildFrames(); } void nsHTMLScrollFrame::RemoveFrame(DestroyContext& aContext, ChildListID aListID, nsIFrame* aOldFrame) { NS_ASSERTION(aListID == FrameChildListID::Principal, "Only main list supported"); mFrames.DestroyFrame(aContext, aOldFrame); ReloadChildFrames(); } /** HTML scrolling implementation All other things being equal, we prefer layouts with fewer scrollbars showing. */ namespace mozilla { enum class ShowScrollbar : uint8_t { Auto, Always, // Never is a misnomer. We can still get a scrollbar if we need to scroll the // visual viewport inside the layout viewport. Thus this enum is best thought // of as value used by layout, which does not know about the visual viewport. // The visual viewport does not affect any layout sizes, so this is sound. Never, }; static ShowScrollbar ShouldShowScrollbar(StyleOverflow aOverflow) { switch (aOverflow) { case StyleOverflow::Scroll: return ShowScrollbar::Always; case StyleOverflow::Hidden: return ShowScrollbar::Never; default: case StyleOverflow::Auto: return ShowScrollbar::Auto; } } struct MOZ_STACK_CLASS ScrollReflowInput { // === Filled in by the constructor. Members in this section shouldn't change // their values after the constructor. === const ReflowInput& mReflowInput; ShowScrollbar mHScrollbar; // If the horizontal scrollbar is allowed (even if mHScrollbar == // ShowScrollbar::Never) provided that it is for scrolling the visual viewport // inside the layout viewport only. bool mHScrollbarAllowedForScrollingVVInsideLV = true; ShowScrollbar mVScrollbar; // If the vertical scrollbar is allowed (even if mVScrollbar == // ShowScrollbar::Never) provided that it is for scrolling the visual viewport // inside the layout viewport only. bool mVScrollbarAllowedForScrollingVVInsideLV = true; nsMargin mComputedBorder; // === Filled in by ReflowScrolledFrame === OverflowAreas mContentsOverflowAreas; // The scrollbar gutter sizes used in the most recent reflow of // mScrolledFrame. The writing-mode is the same as the scroll // container. LogicalMargin mScrollbarGutterFromLastReflow; // True if the most recent reflow of mScrolledFrame is with the // horizontal scrollbar. bool mReflowedContentsWithHScrollbar = false; // True if the most recent reflow of mScrolledFrame is with the // vertical scrollbar. bool mReflowedContentsWithVScrollbar = false; // === Filled in when TryLayout succeeds === // The size of the inside-border area nsSize mInsideBorderSize; // Whether we decided to show the horizontal scrollbar in the most recent // TryLayout. bool mShowHScrollbar = false; // Whether we decided to show the vertical scrollbar in the most recent // TryLayout. bool mShowVScrollbar = false; // If mShow(H|V)Scrollbar is true then // mOnlyNeed(V|H)ScrollbarToScrollVVInsideLV indicates if the only reason we // need that scrollbar is to scroll the visual viewport inside the layout // viewport. These scrollbars are special in that even if they are layout // scrollbars they do not take up any layout space. bool mOnlyNeedHScrollbarToScrollVVInsideLV = false; bool mOnlyNeedVScrollbarToScrollVVInsideLV = false; ScrollReflowInput(nsHTMLScrollFrame* aFrame, const ReflowInput& aReflowInput); nscoord VScrollbarMinHeight() const { return mVScrollbarPrefSize.height; } nscoord VScrollbarPrefWidth() const { return mVScrollbarPrefSize.width; } nscoord HScrollbarMinWidth() const { return mHScrollbarPrefSize.width; } nscoord HScrollbarPrefHeight() const { return mHScrollbarPrefSize.height; } // Returns the sizes occupied by the scrollbar gutters. If aShowVScroll or // aShowHScroll is true, the sizes occupied by the scrollbars are also // included. nsMargin ScrollbarGutter(bool aShowVScrollbar, bool aShowHScrollbar, bool aScrollbarOnRight) const { if (mOverlayScrollbars) { return mScrollbarGutter; } nsMargin gutter = mScrollbarGutter; if (aShowVScrollbar && gutter.right == 0 && gutter.left == 0) { const nscoord w = VScrollbarPrefWidth(); if (aScrollbarOnRight) { gutter.right = w; } else { gutter.left = w; } } if (aShowHScrollbar && gutter.bottom == 0) { // The horizontal scrollbar is always at the bottom side. gutter.bottom = HScrollbarPrefHeight(); } return gutter; } bool OverlayScrollbars() const { return mOverlayScrollbars; } private: // Filled in by the constructor. Put variables here to keep them unchanged // after initializing them in the constructor. nsSize mVScrollbarPrefSize; nsSize mHScrollbarPrefSize; bool mOverlayScrollbars = false; // The scrollbar gutter sizes resolved from the scrollbar-gutter and // scrollbar-width property. nsMargin mScrollbarGutter; }; ScrollReflowInput::ScrollReflowInput(nsHTMLScrollFrame* aFrame, const ReflowInput& aReflowInput) : mReflowInput(aReflowInput), mComputedBorder(aReflowInput.ComputedPhysicalBorderPadding() - aReflowInput.ComputedPhysicalPadding()), mScrollbarGutterFromLastReflow(aFrame->GetWritingMode()) { ScrollStyles styles = aFrame->GetScrollStyles(); mHScrollbar = ShouldShowScrollbar(styles.mHorizontal); mVScrollbar = ShouldShowScrollbar(styles.mVertical); mOverlayScrollbars = aFrame->UsesOverlayScrollbars(); if (nsScrollbarFrame* scrollbar = aFrame->GetScrollbarBox(false)) { scrollbar->SetScrollbarMediatorContent(mReflowInput.mFrame->GetContent()); mHScrollbarPrefSize = scrollbar->ScrollbarMinSize(); // A zero minimum size is a bug with non-overlay scrollbars. That means // we'll always try to place the scrollbar, even if it will ultimately not // fit, see bug 1809630. XUL collapsing is the exception because the // front-end uses it. MOZ_ASSERT(mHScrollbarPrefSize.width && mHScrollbarPrefSize.height, "Shouldn't have a zero horizontal scrollbar-size"); } else { mHScrollbar = ShowScrollbar::Never; mHScrollbarAllowedForScrollingVVInsideLV = false; } if (nsScrollbarFrame* scrollbar = aFrame->GetScrollbarBox(true)) { scrollbar->SetScrollbarMediatorContent(mReflowInput.mFrame->GetContent()); mVScrollbarPrefSize = scrollbar->ScrollbarMinSize(); // See above. MOZ_ASSERT(mVScrollbarPrefSize.width && mVScrollbarPrefSize.height, "Shouldn't have a zero vertical scrollbar-size"); } else { mVScrollbar = ShowScrollbar::Never; mVScrollbarAllowedForScrollingVVInsideLV = false; } const auto* scrollbarStyle = nsLayoutUtils::StyleForScrollbar(mReflowInput.mFrame); // Hide the scrollbar when the scrollbar-width is set to none. // // Note: In some cases this is unnecessary, because scrollbar-width:none // makes us suppress scrollbars in CreateAnonymousContent. But if this frame // initially had a non-'none' scrollbar-width and dynamically changed to // 'none', then we'll need to handle it here. const auto scrollbarWidth = scrollbarStyle->StyleUIReset()->ScrollbarWidth(); if (scrollbarWidth == StyleScrollbarWidth::None) { mHScrollbar = ShowScrollbar::Never; mHScrollbarAllowedForScrollingVVInsideLV = false; mVScrollbar = ShowScrollbar::Never; mVScrollbarAllowedForScrollingVVInsideLV = false; } mScrollbarGutter = aFrame->ComputeStableScrollbarGutter( scrollbarWidth, scrollbarStyle->StyleDisplay()->mScrollbarGutter); } } // namespace mozilla static nsSize ComputeInsideBorderSize(const ScrollReflowInput& aState, const nsSize& aDesiredInsideBorderSize) { // aDesiredInsideBorderSize is the frame size; i.e., it includes // borders and padding (but the scrolled child doesn't have // borders). The scrolled child has the same padding as us. const WritingMode wm = aState.mReflowInput.GetWritingMode(); const LogicalSize desiredInsideBorderSize(wm, aDesiredInsideBorderSize); LogicalSize contentSize = aState.mReflowInput.ComputedSize(); const LogicalMargin padding = aState.mReflowInput.ComputedLogicalPadding(wm); if (contentSize.ISize(wm) == NS_UNCONSTRAINEDSIZE) { contentSize.ISize(wm) = desiredInsideBorderSize.ISize(wm) - padding.IStartEnd(wm); } if (contentSize.BSize(wm) == NS_UNCONSTRAINEDSIZE) { contentSize.BSize(wm) = desiredInsideBorderSize.BSize(wm) - padding.BStartEnd(wm); } contentSize.ISize(wm) = aState.mReflowInput.ApplyMinMaxISize(contentSize.ISize(wm)); contentSize.BSize(wm) = aState.mReflowInput.ApplyMinMaxBSize(contentSize.BSize(wm)); return (contentSize + padding.Size(wm)).GetPhysicalSize(wm); } /** * Assuming that we know the metrics for our wrapped frame and * whether the horizontal and/or vertical scrollbars are present, * compute the resulting layout and return true if the layout is * consistent. If the layout is consistent then we fill in the * computed fields of the ScrollReflowInput. * * The layout is consistent when both scrollbars are showing if and only * if they should be showing. A horizontal scrollbar should be showing if all * following conditions are met: * 1) the style is not HIDDEN * 2) our inside-border height is at least the scrollbar height (i.e., the * scrollbar fits vertically) * 3) the style is SCROLL, or the kid's overflow-area XMost is * greater than the scrollport width * * @param aForce if true, then we just assume the layout is consistent. */ bool nsHTMLScrollFrame::TryLayout(ScrollReflowInput& aState, ReflowOutput* aKidMetrics, bool aAssumeHScroll, bool aAssumeVScroll, bool aForce) { if ((aState.mVScrollbar == ShowScrollbar::Never && aAssumeVScroll) || (aState.mHScrollbar == ShowScrollbar::Never && aAssumeHScroll)) { NS_ASSERTION(!aForce, "Shouldn't be forcing a hidden scrollbar to show!"); return false; } const auto wm = GetWritingMode(); const nsMargin scrollbarGutter = aState.ScrollbarGutter( aAssumeVScroll, aAssumeHScroll, IsScrollbarOnRight()); const LogicalMargin logicalScrollbarGutter(wm, scrollbarGutter); const bool inlineEndsGutterChanged = aState.mScrollbarGutterFromLastReflow.IStartEnd(wm) != logicalScrollbarGutter.IStartEnd(wm); const bool blockEndsGutterChanged = aState.mScrollbarGutterFromLastReflow.BStartEnd(wm) != logicalScrollbarGutter.BStartEnd(wm); const bool shouldReflowScrolledFrame = inlineEndsGutterChanged || (blockEndsGutterChanged && ScrolledContentDependsOnBSize(aState)); if (shouldReflowScrolledFrame) { if (blockEndsGutterChanged) { nsLayoutUtils::MarkIntrinsicISizesDirtyIfDependentOnBSize(mScrolledFrame); } aKidMetrics->mOverflowAreas.Clear(); ROOT_SCROLLBAR_LOG( "TryLayout reflowing scrolled frame with scrollbars h=%d, v=%d\n", aAssumeHScroll, aAssumeVScroll); ReflowScrolledFrame(aState, aAssumeHScroll, aAssumeVScroll, aKidMetrics); } const nsSize scrollbarGutterSize(scrollbarGutter.LeftRight(), scrollbarGutter.TopBottom()); // First, compute our inside-border size and scrollport size nsSize kidSize = GetContainSizeAxes().ContainSize( aKidMetrics->PhysicalSize(), *aState.mReflowInput.mFrame); const nsSize desiredInsideBorderSize = kidSize + scrollbarGutterSize; aState.mInsideBorderSize = ComputeInsideBorderSize(aState, desiredInsideBorderSize); nsSize layoutSize = mIsUsingMinimumScaleSize ? mMinimumScaleSize : aState.mInsideBorderSize; const nsSize scrollPortSize = Max(nsSize(0, 0), layoutSize - scrollbarGutterSize); if (mIsUsingMinimumScaleSize) { mICBSize = Max(nsSize(0, 0), aState.mInsideBorderSize - scrollbarGutterSize); } nsSize visualViewportSize = scrollPortSize; ROOT_SCROLLBAR_LOG("TryLayout with VV %s\n", ToString(visualViewportSize).c_str()); mozilla::PresShell* presShell = PresShell(); // Note: we check for a non-null MobileViepwortManager here, but ideally we // should be able to drop that clause as well. It's just that in some cases // with extension popups the composition size comes back as stale, because // the content viewer is only resized after the popup contents are reflowed. // That case also happens to have no APZ and no MVM, so we use that as a // way to detect the scenario. Bug 1648669 tracks removing this clause. if (mIsRoot && presShell->GetMobileViewportManager()) { visualViewportSize = nsLayoutUtils::CalculateCompositionSizeForFrame( this, false, &layoutSize); visualViewportSize = Max(nsSize(0, 0), visualViewportSize - scrollbarGutterSize); float resolution = presShell->GetResolution(); visualViewportSize.width /= resolution; visualViewportSize.height /= resolution; ROOT_SCROLLBAR_LOG("TryLayout now with VV %s\n", ToString(visualViewportSize).c_str()); } nsRect overflowRect = aState.mContentsOverflowAreas.ScrollableOverflow(); // If the content height expanded by the minimum-scale will be taller than // the scrollable overflow area, we need to expand the area here to tell // properly whether we need to render the overlay vertical scrollbar. // NOTE: This expanded size should NOT be used for non-overley scrollbars // cases since putting the vertical non-overlay scrollbar will make the // content width narrow a little bit, which in turn the minimum scale value // becomes a bit bigger than before, then the vertical scrollbar is no longer // needed, which means the content width becomes the original width, then the // minimum-scale is changed to the original one, and so forth. if (UsesOverlayScrollbars() && mIsUsingMinimumScaleSize && mMinimumScaleSize.height > overflowRect.YMost()) { overflowRect.height += mMinimumScaleSize.height - overflowRect.YMost(); } nsRect scrolledRect = GetUnsnappedScrolledRectInternal(overflowRect, scrollPortSize); ROOT_SCROLLBAR_LOG( "TryLayout scrolledRect:%s overflowRect:%s scrollportSize:%s\n", ToString(scrolledRect).c_str(), ToString(overflowRect).c_str(), ToString(scrollPortSize).c_str()); nscoord oneDevPixel = PresContext()->DevPixelsToAppUnits(1); bool showHScrollbar = aAssumeHScroll; bool showVScrollbar = aAssumeVScroll; if (!aForce) { nsSize sizeToCompare = visualViewportSize; if (gfxPlatform::UseDesktopZoomingScrollbars()) { sizeToCompare = scrollPortSize; } // No need to compute showHScrollbar if we got ShowScrollbar::Never. if (aState.mHScrollbar != ShowScrollbar::Never) { showHScrollbar = aState.mHScrollbar == ShowScrollbar::Always || scrolledRect.XMost() >= sizeToCompare.width + oneDevPixel || scrolledRect.x <= -oneDevPixel; // TODO(emilio): This should probably check this scrollbar's minimum size // in both axes, for consistency? if (aState.mHScrollbar == ShowScrollbar::Auto && scrollPortSize.width < aState.HScrollbarMinWidth()) { showHScrollbar = false; } ROOT_SCROLLBAR_LOG("TryLayout wants H Scrollbar: %d =? %d\n", showHScrollbar, aAssumeHScroll); } // No need to compute showVScrollbar if we got ShowScrollbar::Never. if (aState.mVScrollbar != ShowScrollbar::Never) { showVScrollbar = aState.mVScrollbar == ShowScrollbar::Always || scrolledRect.YMost() >= sizeToCompare.height + oneDevPixel || scrolledRect.y <= -oneDevPixel; // TODO(emilio): This should probably check this scrollbar's minimum size // in both axes, for consistency? if (aState.mVScrollbar == ShowScrollbar::Auto && scrollPortSize.height < aState.VScrollbarMinHeight()) { showVScrollbar = false; } ROOT_SCROLLBAR_LOG("TryLayout wants V Scrollbar: %d =? %d\n", showVScrollbar, aAssumeVScroll); } if (showHScrollbar != aAssumeHScroll || showVScrollbar != aAssumeVScroll) { const nsMargin wantedScrollbarGutter = aState.ScrollbarGutter( showVScrollbar, showHScrollbar, IsScrollbarOnRight()); // We report an inconsistent layout only when the desired visibility of // the scrollbars can change the size of the scrollbar gutters. if (scrollbarGutter != wantedScrollbarGutter) { return false; } } } // If we reach here, the layout is consistent. Record the desired visibility // of the scrollbars. aState.mShowHScrollbar = showHScrollbar; aState.mShowVScrollbar = showVScrollbar; const nsPoint scrollPortOrigin( aState.mComputedBorder.left + scrollbarGutter.left, aState.mComputedBorder.top + scrollbarGutter.top); SetScrollPort(nsRect(scrollPortOrigin, scrollPortSize)); if (mIsRoot && gfxPlatform::UseDesktopZoomingScrollbars()) { bool vvChanged = true; const bool overlay = aState.OverlayScrollbars(); // This loop can run at most twice since we can only add a scrollbar once. // At this point we've already decided that this layout is consistent so we // will return true. Scrollbars added here never take up layout space even // if they are layout scrollbars so any changes made here will not make us // return false. while (vvChanged) { vvChanged = false; if (!aState.mShowHScrollbar && aState.mHScrollbarAllowedForScrollingVVInsideLV) { if (ScrollPort().width >= visualViewportSize.width + oneDevPixel && (overlay || visualViewportSize.width >= aState.HScrollbarMinWidth())) { vvChanged = true; if (!overlay) { visualViewportSize.height -= aState.HScrollbarPrefHeight(); } aState.mShowHScrollbar = true; aState.mOnlyNeedHScrollbarToScrollVVInsideLV = true; ROOT_SCROLLBAR_LOG("TryLayout added H scrollbar for VV, VV now %s\n", ToString(visualViewportSize).c_str()); } } if (!aState.mShowVScrollbar && aState.mVScrollbarAllowedForScrollingVVInsideLV) { if (ScrollPort().height >= visualViewportSize.height + oneDevPixel && (overlay || visualViewportSize.height >= aState.VScrollbarMinHeight())) { vvChanged = true; if (!overlay) { visualViewportSize.width -= aState.VScrollbarPrefWidth(); } aState.mShowVScrollbar = true; aState.mOnlyNeedVScrollbarToScrollVVInsideLV = true; ROOT_SCROLLBAR_LOG("TryLayout added V scrollbar for VV, VV now %s\n", ToString(visualViewportSize).c_str()); } } } } return true; } bool nsHTMLScrollFrame::ScrolledContentDependsOnBSize( const ScrollReflowInput& aState) const { return mScrolledFrame->HasAnyStateBits( NS_FRAME_CONTAINS_RELATIVE_BSIZE | NS_FRAME_DESCENDANT_INTRINSIC_ISIZE_DEPENDS_ON_BSIZE) || aState.mReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE || aState.mReflowInput.ComputedMinBSize() > 0 || aState.mReflowInput.ComputedMaxBSize() != NS_UNCONSTRAINEDSIZE; } void nsHTMLScrollFrame::ReflowScrolledFrame(ScrollReflowInput& aState, bool aAssumeHScroll, bool aAssumeVScroll, ReflowOutput* aMetrics) { const WritingMode wm = GetWritingMode(); // these could be NS_UNCONSTRAINEDSIZE ... std::min arithmetic should // be OK LogicalMargin padding = aState.mReflowInput.ComputedLogicalPadding(wm); nscoord availISize = aState.mReflowInput.ComputedISize() + padding.IStartEnd(wm); nscoord computedBSize = aState.mReflowInput.ComputedBSize(); nscoord computedMinBSize = aState.mReflowInput.ComputedMinBSize(); nscoord computedMaxBSize = aState.mReflowInput.ComputedMaxBSize(); if (!ShouldPropagateComputedBSizeToScrolledContent()) { computedBSize = NS_UNCONSTRAINEDSIZE; computedMinBSize = 0; computedMaxBSize = NS_UNCONSTRAINEDSIZE; } const LogicalMargin scrollbarGutter( wm, aState.ScrollbarGutter(aAssumeVScroll, aAssumeHScroll, IsScrollbarOnRight())); if (const nscoord inlineEndsGutter = scrollbarGutter.IStartEnd(wm); inlineEndsGutter > 0) { availISize = std::max(0, availISize - inlineEndsGutter); } if (const nscoord blockEndsGutter = scrollbarGutter.BStartEnd(wm); blockEndsGutter > 0) { if (computedBSize != NS_UNCONSTRAINEDSIZE) { computedBSize = std::max(0, computedBSize - blockEndsGutter); } computedMinBSize = std::max(0, computedMinBSize - blockEndsGutter); if (computedMaxBSize != NS_UNCONSTRAINEDSIZE) { computedMaxBSize = std::max(0, computedMaxBSize - blockEndsGutter); } } nsPresContext* presContext = PresContext(); // Pass InitFlags::CallerWillInit so we can pass in the correct padding. ReflowInput kidReflowInput(presContext, aState.mReflowInput, mScrolledFrame, LogicalSize(wm, availISize, NS_UNCONSTRAINEDSIZE), Nothing(), ReflowInput::InitFlag::CallerWillInit); const WritingMode kidWM = kidReflowInput.GetWritingMode(); kidReflowInput.Init(presContext, Nothing(), Nothing(), Some(padding.ConvertTo(kidWM, wm))); kidReflowInput.mFlags.mAssumingHScrollbar = aAssumeHScroll; kidReflowInput.mFlags.mAssumingVScrollbar = aAssumeVScroll; kidReflowInput.mFlags.mTreatBSizeAsIndefinite = aState.mReflowInput.mFlags.mTreatBSizeAsIndefinite; kidReflowInput.SetComputedBSize(computedBSize); kidReflowInput.SetComputedMinBSize(computedMinBSize); kidReflowInput.SetComputedMaxBSize(computedMaxBSize); if (aState.mReflowInput.IsBResizeForWM(kidWM)) { kidReflowInput.SetBResize(true); } if (aState.mReflowInput.IsBResizeForPercentagesForWM(kidWM)) { kidReflowInput.mFlags.mIsBResizeForPercentages = true; } // Temporarily set mHasHorizontalScrollbar/mHasVerticalScrollbar to // reflect our assumptions while we reflow the child. bool didHaveHorizontalScrollbar = mHasHorizontalScrollbar; bool didHaveVerticalScrollbar = mHasVerticalScrollbar; mHasHorizontalScrollbar = aAssumeHScroll; mHasVerticalScrollbar = aAssumeVScroll; nsReflowStatus status; // No need to pass a true container-size to ReflowChild or // FinishReflowChild, because it's only used there when positioning // the frame (i.e. if ReflowChildFlags::NoMoveFrame isn't set) const nsSize dummyContainerSize; ReflowChild(mScrolledFrame, presContext, *aMetrics, kidReflowInput, wm, LogicalPoint(wm), dummyContainerSize, ReflowChildFlags::NoMoveFrame, status); mHasHorizontalScrollbar = didHaveHorizontalScrollbar; mHasVerticalScrollbar = didHaveVerticalScrollbar; // Don't resize or position the view (if any) because we're going to resize // it to the correct size anyway in PlaceScrollArea. Allowing it to // resize here would size it to the natural height of the frame, // which will usually be different from the scrollport height; // invalidating the difference will cause unnecessary repainting. FinishReflowChild( mScrolledFrame, presContext, *aMetrics, &kidReflowInput, wm, LogicalPoint(wm), dummyContainerSize, ReflowChildFlags::NoMoveFrame | ReflowChildFlags::NoSizeView); if (mScrolledFrame->HasAnyStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE)) { // Propagate NS_FRAME_CONTAINS_RELATIVE_BSIZE from our inner scrolled frame // to ourselves so that our containing block is aware of it. // // Note: If the scrolled frame has any child whose block-size depends on the // containing block's block-size, the NS_FRAME_CONTAINS_RELATIVE_BSIZE bit // is set on the scrolled frame when initializing the child's ReflowInput in // ReflowInput::InitResizeFlags(). Therefore, we propagate the bit here // after we reflowed the scrolled frame. AddStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE); } // XXX Some frames (e.g. nsFrameFrame, nsTextFrame) don't // bother setting their mOverflowArea. This is wrong because every frame // should always set mOverflowArea. In fact nsFrameFrame doesn't // support the 'outline' property because of this. Rather than fix the // world right now, just fix up the overflow area if necessary. Note that we // don't check HasOverflowRect() because it could be set even though the // overflow area doesn't include the frame bounds. aMetrics->UnionOverflowAreasWithDesiredBounds(); auto* disp = StyleDisplay(); if (MOZ_UNLIKELY(disp->mOverflowClipBoxInline == StyleOverflowClipBox::ContentBox)) { // The scrolled frame is scrollable in the inline axis with // `overflow-clip-box:content-box`. To prevent its content from being // clipped at the scroll container's padding edges, we inflate its // children's scrollable overflow area with its inline padding, and union // its scrollable overflow area with its children's inflated scrollable // overflow area. OverflowAreas childOverflow; mScrolledFrame->UnionChildOverflow(childOverflow); nsRect childScrollableOverflow = childOverflow.ScrollableOverflow(); const LogicalMargin inlinePadding = padding.ApplySkipSides(LogicalSides(wm, eLogicalSideBitsBBoth)); childScrollableOverflow.Inflate(inlinePadding.GetPhysicalMargin(wm)); nsRect& so = aMetrics->ScrollableOverflow(); so = so.UnionEdges(childScrollableOverflow); } aState.mContentsOverflowAreas = aMetrics->mOverflowAreas; aState.mScrollbarGutterFromLastReflow = scrollbarGutter; aState.mReflowedContentsWithHScrollbar = aAssumeHScroll; aState.mReflowedContentsWithVScrollbar = aAssumeVScroll; } bool nsHTMLScrollFrame::GuessHScrollbarNeeded(const ScrollReflowInput& aState) { if (aState.mHScrollbar != ShowScrollbar::Auto) { // no guessing required return aState.mHScrollbar == ShowScrollbar::Always; } // We only care about scrollbars that might take up space when trying to guess // if we need a scrollbar, so we ignore scrollbars only created to scroll the // visual viewport inside the layout viewport because they take up no layout // space. return mHasHorizontalScrollbar && !mOnlyNeedHScrollbarToScrollVVInsideLV; } bool nsHTMLScrollFrame::GuessVScrollbarNeeded(const ScrollReflowInput& aState) { if (aState.mVScrollbar != ShowScrollbar::Auto) { // no guessing required return aState.mVScrollbar == ShowScrollbar::Always; } // If we've had at least one non-initial reflow, then just assume // the state of the vertical scrollbar will be what we determined // last time. if (mHadNonInitialReflow) { // We only care about scrollbars that might take up space when trying to // guess if we need a scrollbar, so we ignore scrollbars only created to // scroll the visual viewport inside the layout viewport because they take // up no layout space. return mHasVerticalScrollbar && !mOnlyNeedVScrollbarToScrollVVInsideLV; } // If this is the initial reflow, guess false because usually // we have very little content by then. if (InInitialReflow()) return false; if (mIsRoot) { nsIFrame* f = mScrolledFrame->PrincipalChildList().FirstChild(); if (f && f->IsSVGOuterSVGFrame() && static_cast(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 !mIsRoot && HasAnyStateBits(NS_FRAME_FIRST_REFLOW); } void nsHTMLScrollFrame::ReflowContents(ScrollReflowInput& aState, const ReflowOutput& aDesiredSize) { const WritingMode desiredWm = aDesiredSize.GetWritingMode(); ReflowOutput kidDesiredSize(desiredWm); ReflowScrolledFrame(aState, GuessHScrollbarNeeded(aState), GuessVScrollbarNeeded(aState), &kidDesiredSize); // There's an important special case ... if the child appears to fit // in the inside-border rect (but overflows the scrollport), we // should try laying it out without a vertical scrollbar. It will // usually fit because making the available-width wider will not // normally make the child taller. (The only situation I can think // of is when you have a line containing %-width inline replaced // elements whose percentages sum to more than 100%, so increasing // the available width makes the line break where it was fitting // before.) If we don't treat this case specially, then we will // decide that showing scrollbars is OK because the content // overflows when we're showing scrollbars and we won't try to // remove the vertical scrollbar. // Detecting when we enter this special case is important for when // people design layouts that exactly fit the container "most of the // time". // XXX Is this check really sufficient to catch all the incremental cases // where the ideal case doesn't have a scrollbar? if ((aState.mReflowedContentsWithHScrollbar || aState.mReflowedContentsWithVScrollbar) && aState.mVScrollbar != ShowScrollbar::Always && aState.mHScrollbar != ShowScrollbar::Always) { nsSize kidSize = GetContainSizeAxes().ContainSize( kidDesiredSize.PhysicalSize(), *aState.mReflowInput.mFrame); nsSize insideBorderSize = ComputeInsideBorderSize(aState, kidSize); nsRect scrolledRect = GetUnsnappedScrolledRectInternal( kidDesiredSize.ScrollableOverflow(), insideBorderSize); if (nsRect(nsPoint(0, 0), insideBorderSize).Contains(scrolledRect)) { // Let's pretend we had no scrollbars coming in here kidDesiredSize.mOverflowAreas.Clear(); ReflowScrolledFrame(aState, false, false, &kidDesiredSize); } } if (IsRootScrollFrameOfDocument()) { UpdateMinimumScaleSize(aState.mContentsOverflowAreas.ScrollableOverflow(), kidDesiredSize.PhysicalSize()); } // Try vertical scrollbar settings that leave the vertical scrollbar // unchanged. Do this first because changing the vertical scrollbar setting is // expensive, forcing a reflow always. // Try leaving the horizontal scrollbar unchanged first. This will be more // efficient. ROOT_SCROLLBAR_LOG("Trying layout1 with %d, %d\n", aState.mReflowedContentsWithHScrollbar, aState.mReflowedContentsWithVScrollbar); if (TryLayout(aState, &kidDesiredSize, aState.mReflowedContentsWithHScrollbar, aState.mReflowedContentsWithVScrollbar, false)) { return; } ROOT_SCROLLBAR_LOG("Trying layout2 with %d, %d\n", !aState.mReflowedContentsWithHScrollbar, aState.mReflowedContentsWithVScrollbar); if (TryLayout(aState, &kidDesiredSize, !aState.mReflowedContentsWithHScrollbar, aState.mReflowedContentsWithVScrollbar, false)) { return; } // OK, now try toggling the vertical scrollbar. The performance advantage // of trying the status-quo horizontal scrollbar state // does not exist here (we'll have to reflow due to the vertical scrollbar // change), so always try no horizontal scrollbar first. bool newVScrollbarState = !aState.mReflowedContentsWithVScrollbar; ROOT_SCROLLBAR_LOG("Trying layout3 with %d, %d\n", false, newVScrollbarState); if (TryLayout(aState, &kidDesiredSize, false, newVScrollbarState, false)) { return; } ROOT_SCROLLBAR_LOG("Trying layout4 with %d, %d\n", true, newVScrollbarState); if (TryLayout(aState, &kidDesiredSize, true, newVScrollbarState, false)) { return; } // OK, we're out of ideas. Try again enabling whatever scrollbars we can // enable and force the layout to stick even if it's inconsistent. // This just happens sometimes. ROOT_SCROLLBAR_LOG("Giving up, adding both scrollbars...\n"); TryLayout(aState, &kidDesiredSize, aState.mHScrollbar != ShowScrollbar::Never, aState.mVScrollbar != ShowScrollbar::Never, true); } void nsHTMLScrollFrame::PlaceScrollArea(ScrollReflowInput& aState, const nsPoint& aScrollPosition) { // Set the x,y of the scrolled frame to the correct value mScrolledFrame->SetPosition(ScrollPort().TopLeft() - aScrollPosition); // Recompute our scrollable overflow, taking perspective children into // account. Note that this only recomputes the overflow areas stored on the // helper (which are used to compute scrollable length and scrollbar thumb // sizes) but not the overflow areas stored on the frame. This seems to work // for now, but it's possible that we may need to update both in the future. AdjustForPerspective(aState.mContentsOverflowAreas.ScrollableOverflow()); // Preserve the width or height of empty rects const nsSize portSize = ScrollPort().Size(); nsRect scrolledRect = GetUnsnappedScrolledRectInternal( aState.mContentsOverflowAreas.ScrollableOverflow(), portSize); nsRect scrolledArea = scrolledRect.UnionEdges(nsRect(nsPoint(0, 0), portSize)); // Store the new overflow area. Note that this changes where an outline // of the scrolled frame would be painted, but scrolled frames can't have // outlines (the outline would go on this scrollframe instead). // Using FinishAndStoreOverflow is needed so the overflow rect gets set // correctly. It also messes with the overflow rect in the 'clip' case, but // scrolled frames can't have 'overflow' either. // This needs to happen before SyncFrameViewAfterReflow so // HasOverflowRect() will return the correct value. OverflowAreas overflow(scrolledArea, scrolledArea); mScrolledFrame->FinishAndStoreOverflow(overflow, mScrolledFrame->GetSize()); // Note that making the view *exactly* the size of the scrolled area // is critical, since the view scrolling code uses the size of the // scrolled view to clamp scroll requests. // Normally the mScrolledFrame won't have a view but in some cases it // might create its own. nsContainerFrame::SyncFrameViewAfterReflow( mScrolledFrame->PresContext(), mScrolledFrame, mScrolledFrame->GetView(), scrolledArea, ReflowChildFlags::Default); } nscoord nsHTMLScrollFrame::IntrinsicScrollbarGutterSizeAtInlineEdges() const { const auto wm = GetWritingMode(); const LogicalMargin gutter(wm, IntrinsicScrollbarGutterSize()); return gutter.IStartEnd(wm); } nsMargin nsHTMLScrollFrame::IntrinsicScrollbarGutterSize() const { if (PresContext()->UseOverlayScrollbars()) { // Overlay scrollbars do not consume space per spec. return {}; } const auto* styleForScrollbar = nsLayoutUtils::StyleForScrollbar(this); const auto& styleScrollbarWidth = styleForScrollbar->StyleUIReset()->ScrollbarWidth(); if (styleScrollbarWidth == StyleScrollbarWidth::None) { // Scrollbar shouldn't appear at all with "scrollbar-width: none". return {}; } const auto& styleScrollbarGutter = styleForScrollbar->StyleDisplay()->mScrollbarGutter; nsMargin gutter = ComputeStableScrollbarGutter(styleScrollbarWidth, styleScrollbarGutter); if (gutter.LeftRight() == 0 || gutter.TopBottom() == 0) { // If there is no stable scrollbar-gutter at vertical or horizontal // dimension, check if a scrollbar is always shown at that dimension. ScrollStyles scrollStyles = GetScrollStyles(); const nscoord scrollbarSize = GetNonOverlayScrollbarSize(PresContext(), styleScrollbarWidth); if (gutter.LeftRight() == 0 && scrollStyles.mVertical == StyleOverflow::Scroll) { (IsScrollbarOnRight() ? gutter.right : gutter.left) = scrollbarSize; } if (gutter.TopBottom() == 0 && scrollStyles.mHorizontal == StyleOverflow::Scroll) { // The horizontal scrollbar is always at the bottom side. gutter.bottom = scrollbarSize; } } return gutter; } nsMargin nsHTMLScrollFrame::ComputeStableScrollbarGutter( const StyleScrollbarWidth& aStyleScrollbarWidth, const StyleScrollbarGutter& aStyleScrollbarGutter) const { if (PresContext()->UseOverlayScrollbars()) { // Overlay scrollbars do not consume space per spec. return {}; } if (aStyleScrollbarWidth == StyleScrollbarWidth::None) { // Scrollbar shouldn't appear at all with "scrollbar-width: none". return {}; } if (aStyleScrollbarGutter == StyleScrollbarGutter::AUTO) { // Scrollbars create space depending on the 'overflow' property and whether // the content overflows. Callers need to check this scenario if they want // to consider the space created by the actual scrollbars. return {}; } const bool bothEdges = bool(aStyleScrollbarGutter & StyleScrollbarGutter::BOTH_EDGES); const bool isVerticalWM = GetWritingMode().IsVertical(); const nscoord scrollbarSize = GetNonOverlayScrollbarSize(PresContext(), aStyleScrollbarWidth); nsMargin scrollbarGutter; if (bothEdges) { if (isVerticalWM) { scrollbarGutter.top = scrollbarGutter.bottom = scrollbarSize; } else { scrollbarGutter.left = scrollbarGutter.right = scrollbarSize; } } else { MOZ_ASSERT(bool(aStyleScrollbarGutter & StyleScrollbarGutter::STABLE), "scrollbar-gutter value should be 'stable'!"); if (isVerticalWM) { // The horizontal scrollbar-gutter is always at the bottom side. scrollbarGutter.bottom = scrollbarSize; } else if (IsScrollbarOnRight()) { scrollbarGutter.right = scrollbarSize; } else { scrollbarGutter.left = scrollbarSize; } } return scrollbarGutter; } // Legacy, this sucks! static bool IsMarqueeScrollbox(const nsIFrame& aScrollFrame) { if (!aScrollFrame.GetContent()) { return false; } if (MOZ_LIKELY(!aScrollFrame.GetContent()->HasBeenInUAWidget())) { return false; } MOZ_ASSERT(aScrollFrame.GetParent() && aScrollFrame.GetParent()->GetContent()); return aScrollFrame.GetParent() && HTMLMarqueeElement::FromNodeOrNull( aScrollFrame.GetParent()->GetContent()); } /* virtual */ nscoord nsHTMLScrollFrame::GetMinISize(gfxContext* aRenderingContext) { nscoord result = [&] { if (const Maybe containISize = ContainIntrinsicISize()) { return *containISize; } if (MOZ_UNLIKELY(IsMarqueeScrollbox(*this))) { return 0; } return mScrolledFrame->GetMinISize(aRenderingContext); }(); DISPLAY_MIN_INLINE_SIZE(this, result); return result + IntrinsicScrollbarGutterSizeAtInlineEdges(); } /* virtual */ nscoord nsHTMLScrollFrame::GetPrefISize(gfxContext* aRenderingContext) { const Maybe containISize = ContainIntrinsicISize(); nscoord result = containISize ? *containISize : mScrolledFrame->GetPrefISize(aRenderingContext); DISPLAY_PREF_INLINE_SIZE(this, result); return NSCoordSaturatingAdd(result, IntrinsicScrollbarGutterSizeAtInlineEdges()); } // When we have perspective set on the outer scroll frame, and transformed // children (possibly with preserve-3d) then the effective transform on the // child depends on the offset to the scroll frame, which changes as we scroll. // This perspective transform can cause the element to move relative to the // scrolled inner frame, which would cause the scrollable length changes during // scrolling if we didn't account for it. Since we don't want scrollHeight/Width // and the size of scrollbar thumbs to change during scrolling, we compute the // scrollable overflow by determining the scroll position at which the child // becomes completely visible within the scrollport rather than using the union // of the overflow areas at their current position. static void GetScrollableOverflowForPerspective( nsIFrame* aScrolledFrame, nsIFrame* aCurrentFrame, const nsRect aScrollPort, nsPoint aOffset, nsRect& aScrolledFrameOverflowArea) { // Iterate over all children except pop-ups. for (const auto& [list, listID] : aCurrentFrame->ChildLists()) { if (listID == FrameChildListID::Popup) { continue; } for (nsIFrame* child : list) { nsPoint offset = aOffset; // When we reach a direct child of the scroll, then we record the offset // to convert from that frame's coordinate into the scroll frame's // coordinates. Preserve-3d descendant frames use the same offset as their // ancestors, since TransformRect already converts us into the coordinate // space of the preserve-3d root. if (aScrolledFrame == aCurrentFrame) { offset = child->GetPosition(); } if (child->Extend3DContext()) { // If we're a preserve-3d frame, then recurse and include our // descendants since overflow of preserve-3d frames is only included // in the post-transform overflow area of the preserve-3d root frame. GetScrollableOverflowForPerspective(aScrolledFrame, child, aScrollPort, offset, aScrolledFrameOverflowArea); } // If we're transformed, then we want to consider the possibility that // this frame might move relative to the scrolled frame when scrolling. // For preserve-3d, leaf frames have correct overflow rects relative to // themselves. preserve-3d 'nodes' (intermediate frames and the root) have // only their untransformed children included in their overflow relative // to self, which is what we want to include here. if (child->IsTransformed()) { // Compute the overflow rect for this leaf transform frame in the // coordinate space of the scrolled frame. nsPoint scrollPos = aScrolledFrame->GetPosition(); nsRect preScroll, postScroll; { // TODO: Can we reuse the reference box? TransformReferenceBox refBox(child); preScroll = nsDisplayTransform::TransformRect( child->ScrollableOverflowRectRelativeToSelf(), child, refBox); } // Temporarily override the scroll position of the scrolled frame by // 10 CSS pixels, and then recompute what the overflow rect would be. // This scroll position may not be valid, but that shouldn't matter // for our calculations. { aScrolledFrame->SetPosition(scrollPos + nsPoint(600, 600)); TransformReferenceBox refBox(child); postScroll = nsDisplayTransform::TransformRect( child->ScrollableOverflowRectRelativeToSelf(), child, refBox); aScrolledFrame->SetPosition(scrollPos); } // Compute how many app units the overflow rects moves by when we adjust // the scroll position by 1 app unit. double rightDelta = (postScroll.XMost() - preScroll.XMost() + 600.0) / 600.0; double bottomDelta = (postScroll.YMost() - preScroll.YMost() + 600.0) / 600.0; // We can't ever have negative scrolling. NS_ASSERTION(rightDelta > 0.0f && bottomDelta > 0.0f, "Scrolling can't be reversed!"); // Move preScroll into the coordinate space of the scrollport. preScroll += offset + scrollPos; // For each of the four edges of preScroll, figure out how far they // extend beyond the scrollport. Ignore negative values since that means // that side is already scrolled in to view and we don't need to add // overflow to account for it. nsMargin overhang(std::max(0, aScrollPort.Y() - preScroll.Y()), std::max(0, preScroll.XMost() - aScrollPort.XMost()), std::max(0, preScroll.YMost() - aScrollPort.YMost()), std::max(0, aScrollPort.X() - preScroll.X())); // Scale according to rightDelta/bottomDelta to adjust for the different // scroll rates. overhang.top = NSCoordSaturatingMultiply( overhang.top, static_cast(1 / bottomDelta)); overhang.right = NSCoordSaturatingMultiply( overhang.right, static_cast(1 / rightDelta)); overhang.bottom = NSCoordSaturatingMultiply( overhang.bottom, static_cast(1 / bottomDelta)); overhang.left = NSCoordSaturatingMultiply( overhang.left, static_cast(1 / rightDelta)); // Take the minimum overflow rect that would allow the current scroll // position, using the size of the scroll port and offset by the // inverse of the scroll position. nsRect overflow = aScrollPort - scrollPos; // Expand it by our margins to get an overflow rect that would allow all // edges of our transformed content to be scrolled into view. overflow.Inflate(overhang); // Merge it with the combined overflow aScrolledFrameOverflowArea.UnionRect(aScrolledFrameOverflowArea, overflow); } else if (aCurrentFrame == aScrolledFrame) { aScrolledFrameOverflowArea.UnionRect( aScrolledFrameOverflowArea, child->ScrollableOverflowRectRelativeToParent()); } } } } BaselineSharingGroup nsHTMLScrollFrame::GetDefaultBaselineSharingGroup() const { return mScrolledFrame->GetDefaultBaselineSharingGroup(); } nscoord nsHTMLScrollFrame::SynthesizeFallbackBaseline( mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup) const { // Marign-end even for central baselines. if (aWM.IsLineInverted()) { return -GetLogicalUsedMargin(aWM).BStart(aWM); } return aBaselineGroup == BaselineSharingGroup::First ? BSize(aWM) + GetLogicalUsedMargin(aWM).BEnd(aWM) : -GetLogicalUsedMargin(aWM).BEnd(aWM); } Maybe nsHTMLScrollFrame::GetNaturalBaselineBOffset( WritingMode aWM, BaselineSharingGroup aBaselineGroup, BaselineExportContext aExportContext) const { // Block containers that are scrollable always have a last baseline // that are synthesized from block-end margin edge. // Note(dshin): This behaviour is really only relevant to `inline-block` // alignment context. In the context of table/flex/grid alignment, first/last // baselines are calculated through `GetFirstLineBaseline`, which does // calculations of its own. // https://drafts.csswg.org/css-align/#baseline-export if (aExportContext == BaselineExportContext::LineLayout && aBaselineGroup == BaselineSharingGroup::Last && mScrolledFrame->IsBlockFrameOrSubclass()) { return Some(SynthesizeFallbackBaseline(aWM, aBaselineGroup)); } if (StyleDisplay()->IsContainLayout()) { return Nothing{}; } // OK, here's where we defer to our scrolled frame. return mScrolledFrame ->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext) .map([this, aWM](nscoord aBaseline) { // We have to add our border BStart thickness to whatever it returns, to // produce an offset in our frame-rect's coordinate system. (We don't // have to add padding, because the scrolled frame handles our padding.) LogicalMargin border = GetLogicalUsedBorder(aWM); const auto bSize = GetLogicalSize(aWM).BSize(aWM); // Clamp the baseline to the border rect. See bug 1791069. return std::clamp(border.BStart(aWM) + aBaseline, 0, bSize); }); } void 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(mScrolledFrame, mScrolledFrame, ScrollPort(), nsPoint(), aScrollableOverflow); } void nsHTMLScrollFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, const ReflowInput& aReflowInput, nsReflowStatus& aStatus) { MarkInReflow(); DO_GLOBAL_REFLOW_COUNT("nsHTMLScrollFrame"); DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); HandleScrollbarStyleSwitching(); ScrollReflowInput state(this, aReflowInput); //------------ Handle Incremental Reflow ----------------- bool reflowHScrollbar = true; bool reflowVScrollbar = true; bool reflowScrollCorner = true; if (!aReflowInput.ShouldReflowAllKids()) { auto NeedsReflow = [](const nsIFrame* aFrame) { return aFrame && aFrame->IsSubtreeDirty(); }; reflowHScrollbar = NeedsReflow(mHScrollbarBox); reflowVScrollbar = NeedsReflow(mVScrollbarBox); reflowScrollCorner = NeedsReflow(mScrollCornerBox) || NeedsReflow(mResizerBox); } if (mIsRoot) { reflowScrollCorner = false; } const nsRect oldScrollPort = ScrollPort(); nsRect oldScrolledAreaBounds = mScrolledFrame->ScrollableOverflowRectRelativeToParent(); nsPoint oldScrollPosition = GetScrollPosition(); ReflowContents(state, aDesiredSize); nsSize layoutSize = mIsUsingMinimumScaleSize ? mMinimumScaleSize : state.mInsideBorderSize; aDesiredSize.Width() = layoutSize.width + state.mComputedBorder.LeftRight(); aDesiredSize.Height() = layoutSize.height + state.mComputedBorder.TopBottom(); // Set the size of the frame now since computing the perspective-correct // overflow (within PlaceScrollArea) can rely on it. 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 (!mPostedReflowCallback) { // Make sure we'll try scrolling to restored position PresShell()->PostReflowCallback(this); mPostedReflowCallback = true; } bool didOnlyHScrollbar = mOnlyNeedHScrollbarToScrollVVInsideLV; bool didOnlyVScrollbar = mOnlyNeedVScrollbarToScrollVVInsideLV; mOnlyNeedHScrollbarToScrollVVInsideLV = state.mOnlyNeedHScrollbarToScrollVVInsideLV; mOnlyNeedVScrollbarToScrollVVInsideLV = state.mOnlyNeedVScrollbarToScrollVVInsideLV; bool didHaveHScrollbar = mHasHorizontalScrollbar; bool didHaveVScrollbar = mHasVerticalScrollbar; mHasHorizontalScrollbar = state.mShowHScrollbar; mHasVerticalScrollbar = state.mShowVScrollbar; const nsRect& newScrollPort = ScrollPort(); nsRect newScrolledAreaBounds = mScrolledFrame->ScrollableOverflowRectRelativeToParent(); if (mSkippedScrollbarLayout || reflowHScrollbar || reflowVScrollbar || reflowScrollCorner || HasAnyStateBits(NS_FRAME_IS_DIRTY) || didHaveHScrollbar != state.mShowHScrollbar || didHaveVScrollbar != state.mShowVScrollbar || didOnlyHScrollbar != mOnlyNeedHScrollbarToScrollVVInsideLV || didOnlyVScrollbar != mOnlyNeedVScrollbarToScrollVVInsideLV || !oldScrollPort.IsEqualEdges(newScrollPort) || !oldScrolledAreaBounds.IsEqualEdges(newScrolledAreaBounds)) { if (!mSuppressScrollbarUpdate) { mSkippedScrollbarLayout = false; nsHTMLScrollFrame::SetScrollbarVisibility(mHScrollbarBox, state.mShowHScrollbar); nsHTMLScrollFrame::SetScrollbarVisibility(mVScrollbarBox, state.mShowVScrollbar); // place and reflow scrollbars const nsRect insideBorderArea( nsPoint(state.mComputedBorder.left, state.mComputedBorder.top), layoutSize); LayoutScrollbars(state, insideBorderArea, oldScrollPort); } else { mSkippedScrollbarLayout = true; } } if (mIsRoot) { if (RefPtr manager = PresShell()->GetMobileViewportManager()) { // Note that this runs during layout, and when we get here the root // scrollframe has already been laid out. It may have added or removed // scrollbars as a result of that layout, so we need to ensure the // visual viewport is updated to account for that before we read the // visual viewport size. manager->UpdateVisualViewportSizeForPotentialScrollbarChange(); } else if (oldScrollPort.Size() != newScrollPort.Size()) { // We want to make sure to send a visual viewport resize event if the // scrollport changed sizes for root scroll frames. The // MobileViewportManager will do that, but if we don't have one (ie we // aren't a root content document for example) we have to send one // ourselves. if (auto* window = nsGlobalWindowInner::Cast( aPresContext->Document()->GetInnerWindow())) { window->VisualViewport()->PostResizeEvent(); } } } // Note that we need to do this after the // UpdateVisualViewportSizeForPotentialScrollbarChange call above because that // is what updates the visual viewport size and we need it to be up to date. if (mIsRoot && !state.OverlayScrollbars() && (didHaveHScrollbar != state.mShowHScrollbar || didHaveVScrollbar != state.mShowVScrollbar || didOnlyHScrollbar != mOnlyNeedHScrollbarToScrollVVInsideLV || didOnlyVScrollbar != mOnlyNeedVScrollbarToScrollVVInsideLV) && PresShell()->IsVisualViewportOffsetSet()) { // Removing layout/classic scrollbars can make a previously valid vvoffset // invalid. For example, if we are zoomed in on an overflow hidden document // and then zoom back out, when apz reaches the initial resolution (ie 1.0) // it won't know that we can remove the scrollbars, so the vvoffset can // validly be upto the width/height of the scrollbars. After we reflow and // remove the scrollbars the only valid vvoffset is (0,0). We could wait // until we send the new frame metrics to apz and then have it reply with // the new corrected vvoffset but having an inconsistent vvoffset causes // problems so trigger the vvoffset to be re-set and re-clamped in // ReflowFinished. mReclampVVOffsetInReflowFinished = true; } aDesiredSize.SetOverflowAreasToDesiredBounds(); UpdateSticky(); FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, aStatus); if (!InInitialReflow() && !mHadNonInitialReflow) { mHadNonInitialReflow = true; } if (mIsRoot && !oldScrolledAreaBounds.IsEqualEdges(newScrolledAreaBounds)) { PostScrolledAreaEvent(); } UpdatePrevScrolledRect(); aStatus.Reset(); // This type of frame can't be split. PostOverflowEvent(); } void nsHTMLScrollFrame::DidReflow(nsPresContext* aPresContext, const ReflowInput* aReflowInput) { nsContainerFrame::DidReflow(aPresContext, aReflowInput); if (NeedsResnap()) { PostPendingResnap(); } else { PresShell()->PostPendingScrollAnchorAdjustment(Anchor()); } } //////////////////////////////////////////////////////////////////////////////// #ifdef DEBUG_FRAME_DUMP nsresult nsHTMLScrollFrame::GetFrameName(nsAString& aResult) const { return MakeFrameName(u"HTMLScroll"_ns, aResult); } #endif #ifdef ACCESSIBILITY a11y::AccType nsHTMLScrollFrame::AccessibleType() { if (IsTableCaption()) { return GetRect().IsEmpty() ? a11y::eNoType : a11y::eHTMLCaptionType; } // Create an accessible regardless of focusable state because the state can be // changed during frame life cycle without any notifications to accessibility. if (mContent->IsRootOfNativeAnonymousSubtree() || GetScrollStyles().IsHiddenInBothDirections()) { return a11y::eNoType; } return a11y::eHyperTextType; } #endif NS_QUERYFRAME_HEAD(nsHTMLScrollFrame) NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) NS_QUERYFRAME_ENTRY(nsIScrollableFrame) NS_QUERYFRAME_ENTRY(nsIStatefulFrame) NS_QUERYFRAME_ENTRY(nsIScrollbarMediator) NS_QUERYFRAME_ENTRY(nsHTMLScrollFrame) NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) nsMargin nsHTMLScrollFrame::GetDesiredScrollbarSizes() const { nsPresContext* pc = PresContext(); if (pc->UseOverlayScrollbars()) { return {}; } const auto& style = *nsLayoutUtils::StyleForScrollbar(this); const auto scrollbarWidth = style.StyleUIReset()->ScrollbarWidth(); if (scrollbarWidth == StyleScrollbarWidth::None) { return {}; } ScrollStyles styles = GetScrollStyles(); nsMargin result(0, 0, 0, 0); auto size = GetNonOverlayScrollbarSize(pc, scrollbarWidth); if (styles.mVertical != StyleOverflow::Hidden) { if (IsScrollbarOnRight()) { result.right = size; } else { result.left = size; } } if (styles.mHorizontal != StyleOverflow::Hidden) { // We don't currently support any scripts that would require a scrollbar // at the top. (Are there any?) result.bottom = size; } return result; } nscoord nsHTMLScrollFrame::GetNonOverlayScrollbarSize( const nsPresContext* aPc, StyleScrollbarWidth aScrollbarWidth) { const auto size = aPc->Theme()->GetScrollbarSize(aPc, aScrollbarWidth, nsITheme::Overlay::No); return aPc->DevPixelsToAppUnits(size); } void nsHTMLScrollFrame::HandleScrollbarStyleSwitching() { // Check if we switched between scrollbar styles. if (mScrollbarActivity && !UsesOverlayScrollbars()) { mScrollbarActivity->Destroy(); mScrollbarActivity = nullptr; } else if (!mScrollbarActivity && UsesOverlayScrollbars()) { mScrollbarActivity = new ScrollbarActivity(do_QueryFrame(this)); } } #if 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->IsInNativeAnonymousSubtree()) { aContent = aContent->GetParent(); } return aContent ? nsContentUtils::IsFocusedContent(aContent) : false; } #endif void nsHTMLScrollFrame::SetScrollableByAPZ(bool aScrollable) { mScrollableByAPZ = aScrollable; } void nsHTMLScrollFrame::SetZoomableByAPZ(bool aZoomable) { if (!nsLayoutUtils::UsesAsyncScrolling(this)) { // If APZ is disabled on this window, then we're never actually going to // do any zooming. So we don't need to do any of the setup for it. Note // that this function gets called from ZoomConstraintsClient even if APZ // is disabled to indicate the zoomability of content. aZoomable = false; } if (mZoomableByAPZ != aZoomable) { // We might be changing the result of DecideScrollableLayer() so schedule a // paint to make sure we pick up the result of that change. mZoomableByAPZ = aZoomable; SchedulePaint(); } } void nsHTMLScrollFrame::SetHasOutOfFlowContentInsideFilter() { mHasOutOfFlowContentInsideFilter = true; } bool nsHTMLScrollFrame::WantAsyncScroll() const { ScrollStyles styles = GetScrollStyles(); // First, as an optimization because getting the scrollrange is // relatively slow, check overflow hidden and not a zoomed scroll frame. if (styles.mHorizontal == StyleOverflow::Hidden && styles.mVertical == StyleOverflow::Hidden) { if (!mIsRoot || GetVisualViewportSize() == mScrollPort.Size()) { return false; } } nscoord oneDevPixel = GetScrolledFrame()->PresContext()->AppUnitsPerDevPixel(); nsRect scrollRange = GetLayoutScrollRange(); bool isVScrollable = (scrollRange.height >= oneDevPixel) && (styles.mVertical != StyleOverflow::Hidden); bool isHScrollable = (scrollRange.width >= oneDevPixel) && (styles.mHorizontal != StyleOverflow::Hidden); #if defined(MOZ_WIDGET_ANDROID) // Mobile platforms need focus to scroll text inputs. bool canScrollWithoutScrollbars = !IsForTextControlWithNoScrollbars() || IsFocused(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); if (isVAsyncScrollable || isHAsyncScrollable) { return true; } // If the page has a visual viewport size that's different from // the layout viewport size at the current zoom level, we need to be // able to scroll the visual viewport inside the layout viewport // even if the page is not zoomable. return mIsRoot && GetVisualViewportSize() != mScrollPort.Size() && !GetVisualScrollRange().IsEqualInterior(scrollRange); } static nsRect GetOnePixelRangeAroundPoint(const 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 nsHTMLScrollFrame::ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection, ScrollSnapFlags aSnapFlags) { ScrollByUnit(aScrollbar, ScrollMode::Smooth, aDirection, ScrollUnit::PAGES, aSnapFlags); } void nsHTMLScrollFrame::ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection, ScrollSnapFlags aSnapFlags) { ScrollByUnit(aScrollbar, ScrollMode::Instant, aDirection, ScrollUnit::WHOLE, aSnapFlags); } void nsHTMLScrollFrame::ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection, ScrollSnapFlags aSnapFlags) { bool isHorizontal = aScrollbar->IsHorizontal(); nsIntPoint delta; if (isHorizontal) { const double kScrollMultiplier = StaticPrefs::toolkit_scrollbox_horizontalScrollDistance(); delta.x = static_cast(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 = StaticPrefs::toolkit_scrollbox_verticalScrollDistance(); delta.y = static_cast(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, ScrollUnit::LINES, ScrollMode::Smooth, &overflow, ScrollOrigin::Other, nsIScrollableFrame::NOT_MOMENTUM, aSnapFlags); } void nsHTMLScrollFrame::RepeatButtonScroll(nsScrollbarFrame* aScrollbar) { aScrollbar->MoveToNewPosition(nsScrollbarFrame::ImplementsScrollByUnit::Yes); } void nsHTMLScrollFrame::ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos, nscoord aNewPos) { MOZ_ASSERT(aScrollbar != nullptr); bool isHorizontal = aScrollbar->IsHorizontal(); nsPoint current = GetScrollPosition(); nsPoint dest = current; if (isHorizontal) { dest.x = IsPhysicalLTR() ? aNewPos : aNewPos - GetLayoutScrollRange().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; } ScrollToWithOrigin( dest, &allowedRange, ScrollOperationParams{ScrollMode::Instant, ScrollOrigin::Other}); } void nsHTMLScrollFrame::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, ScrollMode::Smooth); } void nsHTMLScrollFrame::ScrollByUnit(nsScrollbarFrame* aScrollbar, ScrollMode aMode, int32_t aDirection, ScrollUnit aUnit, ScrollSnapFlags aSnapFlags) { MOZ_ASSERT(aScrollbar != nullptr); bool isHorizontal = aScrollbar->IsHorizontal(); nsIntPoint delta; if (isHorizontal) { delta.x = aDirection; } else { delta.y = aDirection; } nsIntPoint overflow; ScrollBy(delta, aUnit, aMode, &overflow, ScrollOrigin::Other, nsIScrollableFrame::NOT_MOMENTUM, aSnapFlags); } //-------------------- Helper ---------------------- // AsyncSmoothMSDScroll has ref counting. class nsHTMLScrollFrame::AsyncSmoothMSDScroll final : public nsARefreshObserver { public: AsyncSmoothMSDScroll(const nsPoint& aInitialPosition, const nsPoint& aInitialDestination, const nsSize& aInitialVelocity, const nsRect& aRange, const mozilla::TimeStamp& aStartTime, nsPresContext* aPresContext, UniquePtr aSnapTargetIds, ScrollTriggeredByScript aTriggeredByScript) : mXAxisModel(aInitialPosition.x, aInitialDestination.x, aInitialVelocity.width, StaticPrefs::layout_css_scroll_behavior_spring_constant(), StaticPrefs::layout_css_scroll_behavior_damping_ratio()), mYAxisModel(aInitialPosition.y, aInitialDestination.y, aInitialVelocity.height, StaticPrefs::layout_css_scroll_behavior_spring_constant(), StaticPrefs::layout_css_scroll_behavior_damping_ratio()), mRange(aRange), mStartPosition(aInitialPosition), mLastRefreshTime(aStartTime), mCallee(nullptr), mOneDevicePixelInAppUnits(aPresContext->DevPixelsToAppUnits(1)), mSnapTargetIds(std::move(aSnapTargetIds)), mTriggeredByScript(aTriggeredByScript) { 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, ScrollTriggeredByScript aTriggeredByScript) { mXAxisModel.SetDestination(static_cast(aDestination.x)); mYAxisModel.SetDestination(static_cast(aDestination.y)); mTriggeredByScript = aTriggeredByScript; } void SetRange(const nsRect& aRange) { mRange = aRange; } nsRect GetRange() { return mRange; } nsPoint GetStartPosition() { return mStartPosition; } 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. nsHTMLScrollFrame::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. */ void SetRefreshObserver(nsHTMLScrollFrame* aCallee) { MOZ_ASSERT(aCallee, "AsyncSmoothMSDScroll::SetRefreshObserver needs " "a non-null aCallee in order to get a refresh driver"); MOZ_RELEASE_ASSERT(!mCallee, "AsyncSmoothMSDScroll::SetRefreshObserver " "shouldn't be called if we're already registered with " "a refresh driver, via a preexisting mCallee"); RefreshDriver(aCallee)->AddRefreshObserver(this, FlushType::Style, "Smooth scroll (MSD) animation"); mCallee = aCallee; } /** * The mCallee holds a strong ref to us since the refresh driver doesn't. * Our dtor and mCallee's Destroy() method both call RemoveObserver() - * whichever comes first removes us from the refresh driver. */ void RemoveObserver() { if (mCallee) { RefreshDriver(mCallee)->RemoveRefreshObserver(this, FlushType::Style); mCallee = nullptr; } } UniquePtr TakeSnapTargetIds() { return std::move(mSnapTargetIds); } bool WasTriggeredByScript() const { return mTriggeredByScript == ScrollTriggeredByScript::Yes; } 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(nsHTMLScrollFrame* aCallee) { return aCallee->PresContext()->RefreshDriver(); } mozilla::layers::AxisPhysicsMSDModel mXAxisModel, mYAxisModel; nsRect mRange; nsPoint mStartPosition; mozilla::TimeStamp mLastRefreshTime; nsHTMLScrollFrame* mCallee; nscoord mOneDevicePixelInAppUnits; UniquePtr mSnapTargetIds; ScrollTriggeredByScript mTriggeredByScript; }; // AsyncScroll has ref counting. class nsHTMLScrollFrame::AsyncScroll final : public nsARefreshObserver { public: typedef mozilla::TimeStamp TimeStamp; typedef mozilla::TimeDuration TimeDuration; explicit AsyncScroll(UniquePtr aSnapTargetIds, ScrollTriggeredByScript aTriggeredByScript) : mOrigin(ScrollOrigin::NotSpecified), mCallee(nullptr), mSnapTargetIds(std::move(aSnapTargetIds)), mTriggeredByScript(aTriggeredByScript) { 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 aInitialPosition, nsPoint aDestination, ScrollOrigin aOrigin, const nsRect& aRange, const nsSize& aCurrentVelocity); void Init(nsPoint aInitialPosition, const nsRect& aRange) { mAnimationPhysics = nullptr; mRange = aRange; mStartPosition = aInitialPosition; } bool IsSmoothScroll() { return mAnimationPhysics != nullptr; } bool IsFinished(const TimeStamp& aTime) const { MOZ_RELEASE_ASSERT(mAnimationPhysics); return mAnimationPhysics->IsFinished(aTime); } nsPoint PositionAt(const TimeStamp& aTime) const { MOZ_RELEASE_ASSERT(mAnimationPhysics); return mAnimationPhysics->PositionAt(aTime); } nsSize VelocityAt(const TimeStamp& aTime) const { MOZ_RELEASE_ASSERT(mAnimationPhysics); return mAnimationPhysics->VelocityAt(aTime); } nsPoint GetStartPosition() const { return mStartPosition; } // Most recent scroll origin. ScrollOrigin mOrigin; // Allowed destination positions around mDestination nsRect mRange; // Initial position where the async scroll was triggered. nsPoint mStartPosition; private: void InitPreferences(TimeStamp aTime, nsAtom* aOrigin); UniquePtr mAnimationPhysics; // The next section is observer/callback management // Bodies of WillRefresh and RefreshDriver contain nsHTMLScrollFrame 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. */ void SetRefreshObserver(nsHTMLScrollFrame* aCallee) { MOZ_ASSERT(aCallee, "AsyncScroll::SetRefreshObserver needs " "a non-null aCallee in order to get a refresh driver"); MOZ_RELEASE_ASSERT(!mCallee, "AsyncScroll::SetRefreshObserver " "shouldn't be called if we're already registered with " "a refresh driver, via a preexisting mCallee"); RefreshDriver(aCallee)->AddRefreshObserver(this, FlushType::Style, "Smooth scroll animation"); mCallee = aCallee; auto* presShell = mCallee->PresShell(); MOZ_ASSERT(presShell); presShell->SuppressDisplayport(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. nsHTMLScrollFrame::AsyncScrollCallback(mCallee, aTime); } /** * The mCallee holds a strong ref to us since the refresh driver doesn't. * Our dtor and mCallee's Destroy() method both call RemoveObserver() - * whichever comes first removes us from the refresh driver. */ void RemoveObserver() { if (mCallee) { RefreshDriver(mCallee)->RemoveRefreshObserver(this, FlushType::Style); auto* presShell = mCallee->PresShell(); MOZ_ASSERT(presShell); presShell->SuppressDisplayport(false); mCallee = nullptr; } } UniquePtr TakeSnapTargetIds() { return std::move(mSnapTargetIds); } bool WasTriggeredByScript() const { return mTriggeredByScript == ScrollTriggeredByScript::Yes; } private: nsHTMLScrollFrame* mCallee; UniquePtr mSnapTargetIds; ScrollTriggeredByScript mTriggeredByScript; nsRefreshDriver* RefreshDriver(nsHTMLScrollFrame* aCallee) { return aCallee->PresContext()->RefreshDriver(); } }; void nsHTMLScrollFrame::AsyncScroll::InitSmoothScroll( TimeStamp aTime, nsPoint aInitialPosition, nsPoint aDestination, ScrollOrigin aOrigin, const nsRect& aRange, const nsSize& aCurrentVelocity) { switch (aOrigin) { case ScrollOrigin::NotSpecified: case ScrollOrigin::Restore: case ScrollOrigin::Relative: // 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 == // ScrollOrigin::Restore here. aOrigin = ScrollOrigin::Other; break; case ScrollOrigin::Apz: // Likewise we should never get APZ-triggered scrolls here, and if that // changes something is likely broken somewhere. MOZ_ASSERT(false); break; default: break; }; // Read preferences only on first iteration or for a different event origin. if (!mAnimationPhysics || aOrigin != mOrigin) { mOrigin = aOrigin; if (StaticPrefs::general_smoothScroll_msdPhysics_enabled()) { mAnimationPhysics = MakeUnique(aInitialPosition); } else { ScrollAnimationBezierPhysicsSettings settings = layers::apz::ComputeBezierAnimationSettingsForOrigin(mOrigin); mAnimationPhysics = MakeUnique(aInitialPosition, settings); } } mStartPosition = aInitialPosition; mRange = aRange; mAnimationPhysics->Update(aTime, aDestination, aCurrentVelocity); } /* * Callback function from AsyncSmoothMSDScroll, used in * nsHTMLScrollFrame::ScrollTo */ void nsHTMLScrollFrame::AsyncSmoothMSDScrollCallback( nsHTMLScrollFrame* 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( aInstance->mAsyncSmoothMSDScroll->GetStartPosition(), range, aInstance->mAsyncSmoothMSDScroll->TakeSnapTargetIds()); } /* * Callback function from AsyncScroll, used in nsHTMLScrollFrame::ScrollTo */ void nsHTMLScrollFrame::AsyncScrollCallback(nsHTMLScrollFrame* 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->IsSmoothScroll()) { 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(aInstance->mAsyncScroll->GetStartPosition(), range, aInstance->mAsyncScroll->TakeSnapTargetIds()); } void nsHTMLScrollFrame::SetTransformingByAPZ(bool aTransforming) { if (mTransformingByAPZ && !aTransforming) { PostScrollEndEvent(); } mTransformingByAPZ = aTransforming; if (!mozilla::css::TextOverflow::HasClippedTextOverflow(this) || mozilla::css::TextOverflow::HasBlockEllipsis(mScrolledFrame)) { // If the block has some overflow marker stuff we should kick off a paint // because we have special behaviour for it when APZ scrolling is active. SchedulePaint(); } } void nsHTMLScrollFrame::CompleteAsyncScroll( const nsPoint& aStartPosition, const nsRect& aRange, UniquePtr aSnapTargetIds, ScrollOrigin aOrigin) { SetLastSnapTargetIds(std::move(aSnapTargetIds)); bool scrollPositionChanged = mDestination != aStartPosition; bool isNotHandledByApz = nsLayoutUtils::CanScrollOriginClobberApz(aOrigin) || ScrollAnimationState().contains(AnimationState::MainThread); // Apply desired destination range since this is the last step of scrolling. RemoveObservers(); AutoWeakFrame weakFrame(this); 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(); // Post a `scrollend` event for scrolling not handled by APZ, including: // // - programmatic instant scrolls // - the end of a smooth scroll animation running on the main thread // // For scrolling handled by APZ, the `scrollend` event is posted in // SetTransformingByAPZ() when the APZC is transitioning from a transforming // to a non-transforming state (e.g. a transition from PANNING to NOTHING). // The scrollend event should not be fired for a scroll that does not // result in a scroll position change. if (isNotHandledByApz && scrollPositionChanged) { PostScrollEndEvent(); } } bool nsHTMLScrollFrame::HasBgAttachmentLocal() const { const nsStyleBackground* bg = StyleBackground(); return bg->HasLocalBackground(); } void nsHTMLScrollFrame::ScrollToInternal( nsPoint aScrollPosition, ScrollMode aMode, ScrollOrigin aOrigin, const nsRect* aRange, ScrollSnapFlags aSnapFlags, ScrollTriggeredByScript aTriggeredByScript) { if (aOrigin == ScrollOrigin::NotSpecified) { aOrigin = ScrollOrigin::Other; } ScrollToWithOrigin( aScrollPosition, aRange, ScrollOperationParams{aMode, aOrigin, aSnapFlags, aTriggeredByScript}); } void nsHTMLScrollFrame::ScrollToCSSPixels(const CSSIntPoint& aScrollPosition, ScrollMode aMode) { CSSIntPoint currentCSSPixels = GetRoundedScrollPositionCSSPixels(); // Transmogrify this scroll to a relative one if there's any on-going // animation in APZ triggered by __user__. // Bug 1740164: We will apply it for cases there's no animation in APZ. auto scrollAnimationState = ScrollAnimationState(); bool isScrollAnimating = scrollAnimationState.contains(AnimationState::MainThread) || scrollAnimationState.contains(AnimationState::APZPending) || scrollAnimationState.contains(AnimationState::APZRequested); if (mCurrentAPZScrollAnimationType == APZScrollAnimationType::TriggeredByUserInput && !isScrollAnimating) { CSSIntPoint delta = aScrollPosition - currentCSSPixels; // This transmogrification need to be an intended end position scroll // operation. ScrollByCSSPixelsInternal(delta, aMode, ScrollSnapFlags::IntendedEndPosition); return; } nscoord halfPixel = nsPresContext::CSSPixelsToAppUnits(0.5f); nsPoint pt = CSSPoint::ToAppUnits(aScrollPosition); 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 nsPoint current = GetScrollPosition(); 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; } ScrollToWithOrigin( pt, &range, ScrollOperationParams{ aMode, ScrollOrigin::Other, // This ScrollToCSSPixels is used for Element.scrollTo, // Element.scrollTop, Element.scrollLeft and for Window.scrollTo. ScrollSnapFlags::IntendedEndPosition, ScrollTriggeredByScript::Yes}); // 'this' might be destroyed here } void nsHTMLScrollFrame::ScrollToCSSPixelsForApz( const CSSPoint& aScrollPosition, ScrollSnapTargetIds&& aLastSnapTargetIds) { 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, &range, ScrollOperationParams{ScrollMode::Instant, ScrollOrigin::Apz, std::move(aLastSnapTargetIds)}); // 'this' might be destroyed here } CSSIntPoint nsHTMLScrollFrame::GetRoundedScrollPositionCSSPixels() { 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 nsHTMLScrollFrame::ScrollToWithOrigin(nsPoint aScrollPosition, const nsRect* aRange, ScrollOperationParams&& aParams) { // None is never a valid scroll origin to be passed in. MOZ_ASSERT(aParams.mOrigin != ScrollOrigin::None); if (aParams.mOrigin != ScrollOrigin::Restore) { // If we're doing a non-restore scroll, we don't want to later // override it by restoring our saved scroll position. SCROLLRESTORE_LOG("%p: Clearing mRestorePos (cur=%s, dst=%s)\n", this, ToString(GetScrollPosition()).c_str(), ToString(aScrollPosition).c_str()); mRestorePos.x = mRestorePos.y = -1; } Maybe snapDestination; if (!aParams.IsScrollSnapDisabled()) { snapDestination = GetSnapPointForDestination(ScrollUnit::DEVICE_PIXELS, aParams.mSnapFlags, mDestination, aScrollPosition); if (snapDestination) { aScrollPosition = snapDestination->mPosition; } } nsRect scrollRange = GetLayoutScrollRange(); mDestination = scrollRange.ClampPoint(aScrollPosition); if (mDestination != aScrollPosition && aParams.mOrigin == ScrollOrigin::Restore && GetPageLoadingState() != LoadingState::Loading) { // 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. aParams.mOrigin = ScrollOrigin::Other; } nsRect range = aRange && snapDestination.isNothing() ? *aRange : nsRect(aScrollPosition, nsSize(0, 0)); UniquePtr snapTargetIds; if (snapDestination) { snapTargetIds = MakeUnique(std::move(snapDestination->mTargetIds)); } else { snapTargetIds = MakeUnique(std::move(aParams.mTargetIds)); } if (aParams.IsInstant()) { // Asynchronous scrolling is not allowed, so we'll kill any existing // async-scrolling process and do an instant scroll. CompleteAsyncScroll(GetScrollPosition(), range, std::move(snapTargetIds), aParams.mOrigin); mApzSmoothScrollDestination = Nothing(); return; } if (!aParams.IsSmoothMsd()) { // 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(); } nsPresContext* presContext = PresContext(); TimeStamp now = presContext->RefreshDriver()->IsTestControllingRefreshesEnabled() ? presContext->RefreshDriver()->MostRecentRefresh() : TimeStamp::Now(); nsSize currentVelocity(0, 0); const bool canHandoffToApz = nsLayoutUtils::AsyncPanZoomEnabled(this) && WantAsyncScroll() && CanApzScrollInTheseDirections( DirectionsInDelta(mDestination - GetScrollPosition())); if (aParams.IsSmoothMsd()) { mIgnoreMomentumScroll = true; if (!mAsyncSmoothMSDScroll) { nsPoint sv = mVelocityQueue.GetVelocity(); currentVelocity.width = sv.x; currentVelocity.height = sv.y; if (mAsyncScroll) { if (mAsyncScroll->IsSmoothScroll()) { currentVelocity = mAsyncScroll->VelocityAt(now); } mAsyncScroll = nullptr; } if (canHandoffToApz) { ApzSmoothScrollTo(mDestination, ScrollMode::SmoothMsd, aParams.mOrigin, aParams.mTriggeredByScript, std::move(snapTargetIds)); return; } mAsyncSmoothMSDScroll = new AsyncSmoothMSDScroll( GetScrollPosition(), mDestination, currentVelocity, GetLayoutScrollRange(), now, presContext, std::move(snapTargetIds), aParams.mTriggeredByScript); mAsyncSmoothMSDScroll->SetRefreshObserver(this); } else { // A previous smooth MSD scroll is still in progress, so we just need to // update its range and destination. mAsyncSmoothMSDScroll->SetRange(GetLayoutScrollRange()); mAsyncSmoothMSDScroll->SetDestination(mDestination, aParams.mTriggeredByScript); } return; } if (mAsyncSmoothMSDScroll) { currentVelocity = mAsyncSmoothMSDScroll->GetVelocity(); mAsyncSmoothMSDScroll = nullptr; } const bool isSmoothScroll = aParams.IsSmooth() && nsLayoutUtils::IsSmoothScrollingEnabled(); if (!mAsyncScroll) { if (isSmoothScroll && canHandoffToApz) { ApzSmoothScrollTo(mDestination, ScrollMode::Smooth, aParams.mOrigin, aParams.mTriggeredByScript, std::move(snapTargetIds)); return; } mAsyncScroll = new AsyncScroll(std::move(snapTargetIds), aParams.mTriggeredByScript); mAsyncScroll->SetRefreshObserver(this); } if (isSmoothScroll) { mAsyncScroll->InitSmoothScroll(now, GetScrollPosition(), mDestination, aParams.mOrigin, range, currentVelocity); } else { mAsyncScroll->Init(GetScrollPosition(), 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->HasAnyStateBits(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. for (const auto& [list, listID] : aFrame->ChildLists()) { if (listID == FrameChildListID::Popup) { continue; } for (nsIFrame* child : list) { AdjustViews(child); } } } void nsHTMLScrollFrame::MarkScrollbarsDirtyForReflow() const { auto* presShell = PresShell(); if (mVScrollbarBox) { presShell->FrameNeedsReflow(mVScrollbarBox, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); } if (mHScrollbarBox) { presShell->FrameNeedsReflow(mHScrollbarBox, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY); } } void nsHTMLScrollFrame::InvalidateScrollbars() const { if (mHScrollbarBox) { mHScrollbarBox->InvalidateFrameSubtree(); } if (mVScrollbarBox) { mVScrollbarBox->InvalidateFrameSubtree(); } } bool nsHTMLScrollFrame::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 && PresContext()->IsRootContentDocumentCrossProcess())) { 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. ScrollStyles styles = GetScrollStyles(); return (styles.mHorizontal != StyleOverflow::Hidden && styles.mVertical != StyleOverflow::Hidden); } static void RemoveDisplayPortCallback(nsITimer* aTimer, void* aClosure) { nsHTMLScrollFrame* sf = static_cast(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(sf->mDisplayPortExpiryTimer); sf->mDisplayPortExpiryTimer = nullptr; if (!sf->AllowDisplayPortExpiration() || sf->mIsParentToActiveScrollFrames) { // 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 // mIsParentToActiveScrollFrames 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. nsIContent* content = sf->GetContent(); if (nsHTMLScrollFrame::ShouldActivateAllScrollFrames()) { // If we are activating all scroll frames then we only want to remove the // regular display port and downgrade to a minimal display port. MOZ_ASSERT(!content->GetProperty(nsGkAtoms::MinimalDisplayPort)); content->SetProperty(nsGkAtoms::MinimalDisplayPort, reinterpret_cast(true)); } else { content->RemoveProperty(nsGkAtoms::MinimalDisplayPort); DisplayPortUtils::RemoveDisplayPort(content); // Be conservative and unflag this this scrollframe as being scrollable by // APZ. If it is still scrollable this will get flipped back soon enough. sf->mScrollableByAPZ = false; } DisplayPortUtils::ExpireDisplayPortOnAsyncScrollableAncestor(sf); sf->SchedulePaint(); } void nsHTMLScrollFrame::MarkEverScrolled() { // 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; } void nsHTMLScrollFrame::MarkNotRecentlyScrolled() { if (!mHasBeenScrolledRecently) return; mHasBeenScrolledRecently = false; SchedulePaint(); } void nsHTMLScrollFrame::MarkRecentlyScrolled() { mHasBeenScrolledRecently = true; if (IsAlwaysActive()) { return; } if (mActivityExpirationState.IsTracked()) { gScrollFrameActivityTracker->MarkUsed(this); } else { if (!gScrollFrameActivityTracker) { gScrollFrameActivityTracker = new ScrollFrameActivityTracker(GetMainThreadSerialEventTarget()); } gScrollFrameActivityTracker->AddObject(this); } // If we just scrolled and there's a displayport expiry timer in place, // reset the timer. ResetDisplayPortExpiryTimer(); } void nsHTMLScrollFrame::ResetDisplayPortExpiryTimer() { if (mDisplayPortExpiryTimer) { mDisplayPortExpiryTimer->InitWithNamedFuncCallback( RemoveDisplayPortCallback, this, StaticPrefs::apz_displayport_expiry_ms(), nsITimer::TYPE_ONE_SHOT, "nsHTMLScrollFrame::ResetDisplayPortExpiryTimer"); } } bool nsHTMLScrollFrame::AllowDisplayPortExpiration() { if (IsAlwaysActive()) { return false; } if (mIsRoot && PresContext()->IsRoot()) { return false; } // If this was the first scrollable frame found, this displayport should // not expire. if (IsFirstScrollableFrameSequenceNumber().isSome()) { return false; } if (ShouldActivateAllScrollFrames() && GetContent()->GetProperty(nsGkAtoms::MinimalDisplayPort)) { return false; } return true; } void nsHTMLScrollFrame::TriggerDisplayPortExpiration() { if (!AllowDisplayPortExpiration()) { return; } if (!StaticPrefs::apz_displayport_expiry_ms()) { // a zero time disables the expiry return; } if (!mDisplayPortExpiryTimer) { mDisplayPortExpiryTimer = NS_NewTimer(); } ResetDisplayPortExpiryTimer(); } void nsHTMLScrollFrame::ScrollVisual() { MarkEverScrolled(); AdjustViews(mScrolledFrame); // We need to call this after fixing up the view positions // to be consistent with the frame hierarchy. 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); if (StaticPrefs::layout_scroll_disable_pixel_alignment()) { return desired; } 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 = aRes == 0.0 ? 0.0 : 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(aBoundUpper - desired) < Abs(desired - aligned)) { return aBoundUpper; } if (aBoundLower == destLower && static_cast(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 = aRes == 0.0 ? 0.0 : 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 MatrixScales& aScale) { return nsPoint( ClampAndAlignWithPixels(aPt.x, aBounds.x, aBounds.XMost(), aRange.x, aRange.XMost(), aAppUnitsPerPixel, aScale.xScale, aCurrent.x), ClampAndAlignWithPixels(aPt.y, aBounds.y, aBounds.YMost(), aRange.y, aRange.YMost(), aAppUnitsPerPixel, aScale.yScale, aCurrent.y)); } /* static */ void nsHTMLScrollFrame::ScrollActivityCallback(nsITimer* aTimer, void* anInstance) { nsHTMLScrollFrame* self = static_cast(anInstance); // Fire the synth mouse move. self->mScrollActivityTimer->Cancel(); self->mScrollActivityTimer = nullptr; self->PresShell()->SynthesizeMouseMove(true); } void nsHTMLScrollFrame::ScheduleSyntheticMouseMove() { if (!mScrollActivityTimer) { mScrollActivityTimer = NS_NewTimer(GetMainThreadSerialEventTarget()); if (!mScrollActivityTimer) { return; } } mScrollActivityTimer->InitWithNamedFuncCallback( ScrollActivityCallback, this, 100, nsITimer::TYPE_ONE_SHOT, "nsHTMLScrollFrame::ScheduleSyntheticMouseMove"); } void nsHTMLScrollFrame::NotifyApproximateFrameVisibilityUpdate( bool aIgnoreDisplayPort) { mLastUpdateFramesPos = GetScrollPosition(); if (aIgnoreDisplayPort) { mHadDisplayPortAtLastFrameUpdate = false; mDisplayPortAtLastFrameUpdate = nsRect(); } else { mHadDisplayPortAtLastFrameUpdate = DisplayPortUtils::GetDisplayPort( GetContent(), &mDisplayPortAtLastFrameUpdate); } } bool nsHTMLScrollFrame::GetDisplayPortAtLastApproximateFrameVisibilityUpdate( nsRect* aDisplayPort) { if (mHadDisplayPortAtLastFrameUpdate) { *aDisplayPort = mDisplayPortAtLastFrameUpdate; } return mHadDisplayPortAtLastFrameUpdate; } /* aIncludeCSSTransform controls if we include CSS transforms that are in this * process (the BrowserChild EffectsInfo mTransformToAncestorScale will include * CSS transforms in ancestor processes in all cases). */ MatrixScales GetPaintedLayerScaleForFrame(nsIFrame* aFrame, bool aIncludeCSSTransform) { MOZ_ASSERT(aFrame, "need a frame"); nsPresContext* presCtx = aFrame->PresContext()->GetRootPresContext(); if (!presCtx) { presCtx = aFrame->PresContext(); MOZ_ASSERT(presCtx); } ParentLayerToScreenScale2D transformToAncestorScale; if (aIncludeCSSTransform) { transformToAncestorScale = nsLayoutUtils::GetTransformToAncestorScaleCrossProcessForFrameMetrics( aFrame); } else { if (BrowserChild* browserChild = BrowserChild::GetFrom(aFrame->PresShell())) { transformToAncestorScale = browserChild->GetEffectsInfo().mTransformToAncestorScale; } } transformToAncestorScale = ParentLayerToParentLayerScale( presCtx->PresShell()->GetCumulativeResolution()) * transformToAncestorScale; return transformToAncestorScale.ToUnknownScale(); } void nsHTMLScrollFrame::ScrollToImpl( nsPoint aPt, const nsRect& aRange, ScrollOrigin aOrigin, ScrollTriggeredByScript aTriggeredByScript) { // None is never a valid scroll origin to be passed in. MOZ_ASSERT(aOrigin != ScrollOrigin::None); // Figure out the effective origin for this scroll request. if (aOrigin == ScrollOrigin::NotSpecified) { // If no origin was specified, we still want to set it to something that's // non-unknown, so that we can use eUnknown to distinguish if the frame was // scrolled at all. Default it to some generic placeholder. aOrigin = ScrollOrigin::Other; } // If this scroll is |relative|, but we've already had a user scroll that // was not relative, promote this origin to |other|. This ensures that we // may only transmit a relative update to APZ if all scrolls since the last // transaction or repaint request have been relative. if (aOrigin == ScrollOrigin::Relative && (mLastScrollOrigin != ScrollOrigin::None && mLastScrollOrigin != ScrollOrigin::NotSpecified && mLastScrollOrigin != ScrollOrigin::Relative && mLastScrollOrigin != ScrollOrigin::Apz)) { aOrigin = ScrollOrigin::Other; } // If the origin is a downgrade, and downgrades are allowed, process the // downgrade even if we're going to early-exit because we're already at // the correct scroll position. This ensures that if there wasn't a main- // thread scroll update pending before a frame reconstruction (as indicated // by mAllowScrollOriginDowngrade=true), then after the frame reconstruction // the origin is downgraded to "restore" even if the layout scroll offset to // be restored is (0,0) (which will take the early-exit below). This is // important so that restoration of a *visual* scroll offset (which might be // to something other than (0,0)) isn't clobbered. bool isScrollOriginDowngrade = nsLayoutUtils::CanScrollOriginClobberApz(mLastScrollOrigin) && !nsLayoutUtils::CanScrollOriginClobberApz(aOrigin); bool allowScrollOriginChange = mAllowScrollOriginDowngrade && isScrollOriginDowngrade; if (allowScrollOriginChange) { mLastScrollOrigin = aOrigin; mAllowScrollOriginDowngrade = false; } nsPresContext* presContext = 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. MatrixScales scale = GetPaintedLayerScaleForFrame( mScrolledFrame, /* aIncludeCSSTransform = */ true); nsPoint curPos = GetScrollPosition(); // 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, GetLayoutScrollRange(), aRange, curPos, appUnitsPerDevPixel, scale); if (pt == curPos) { // Even if we are bailing out due to no-op main-thread scroll position // change, we might need to cancel an APZ smooth scroll that we already // kicked off. It might be reasonable to eventually remove the // mApzSmoothScrollDestination clause from this if statement, as that // may simplify this a bit and should be fine from the APZ side. if (mApzSmoothScrollDestination && aOrigin != ScrollOrigin::Clamp) { if (aOrigin == ScrollOrigin::Relative) { AppendScrollUpdate( ScrollPositionUpdate::NewRelativeScroll(mApzScrollPos, pt)); mApzScrollPos = pt; } else if (aOrigin != ScrollOrigin::Apz) { ScrollOrigin origin = (mAllowScrollOriginDowngrade || !isScrollOriginDowngrade) ? aOrigin : mLastScrollOrigin; AppendScrollUpdate(ScrollPositionUpdate::NewScroll(origin, pt)); } } return; } // If we are scrolling the RCD-RSF, and a visual scroll update is pending, // cancel it; otherwise, it will clobber this scroll. if (IsRootScrollFrameOfDocument() && presContext->IsRootContentDocumentCrossProcess()) { auto* ps = presContext->GetPresShell(); if (const auto& visualScrollUpdate = ps->GetPendingVisualScrollUpdate()) { if (visualScrollUpdate->mVisualScrollOffset != aPt) { // Only clobber if the scroll was originated by the main thread. // Respect the priority of origins (an "eRestore" layout scroll should // not clobber an "eMainThread" visual scroll.) bool shouldClobber = aOrigin == ScrollOrigin::Other || (aOrigin == ScrollOrigin::Restore && visualScrollUpdate->mUpdateType == FrameMetrics::eRestore); if (shouldClobber) { ps->AcknowledgePendingVisualScrollUpdate(); ps->ClearPendingVisualScrollUpdate(); } } } } bool needFrameVisibilityUpdate = mLastUpdateFramesPos == nsPoint(-1, -1); nsPoint dist(std::abs(pt.x - mLastUpdateFramesPos.x), std::abs(pt.y - mLastUpdateFramesPos.y)); nsSize visualViewportSize = GetVisualViewportSize(); nscoord horzAllowance = std::max( visualViewportSize.width / std::max( StaticPrefs:: layout_framevisibility_amountscrollbeforeupdatehorizontal(), 1), AppUnitsPerCSSPixel()); nscoord vertAllowance = std::max( visualViewportSize.height / std::max( StaticPrefs:: layout_framevisibility_amountscrollbeforeupdatevertical(), 1), 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 = GetContent(); DisplayPortUtils::GetDisplayPort(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. allowScrollOriginChange = (mAllowScrollOriginDowngrade || !isScrollOriginDowngrade); if (allowScrollOriginChange) { mLastScrollOrigin = aOrigin; mAllowScrollOriginDowngrade = false; } if (aOrigin == ScrollOrigin::Relative) { MOZ_ASSERT(!isScrollOriginDowngrade); MOZ_ASSERT(mLastScrollOrigin == ScrollOrigin::Relative); AppendScrollUpdate( ScrollPositionUpdate::NewRelativeScroll(mApzScrollPos, pt)); mApzScrollPos = pt; } else if (aOrigin != ScrollOrigin::Apz) { AppendScrollUpdate(ScrollPositionUpdate::NewScroll(mLastScrollOrigin, pt)); } if (mLastScrollOrigin == ScrollOrigin::Apz) { mApzScrollPos = GetScrollPosition(); } ScrollVisual(); mAnchor.UserScrolled(); // Only report user-triggered scrolling interactions bool jsOnStack = nsContentUtils::GetCurrentJSContext() != nullptr; bool scrollingToAnchor = ScrollingInteractionContext::IsScrollingToAnchor(); if (!jsOnStack && !scrollingToAnchor) { nsPoint distanceScrolled(std::abs(pt.x - curPos.x), std::abs(pt.y - curPos.y)); ScrollingMetrics::OnScrollingInteraction( CSSPoint::FromAppUnits(distanceScrolled).Length()); } bool schedulePaint = true; if (nsLayoutUtils::AsyncPanZoomEnabled(this) && !nsLayoutUtils::ShouldDisableApzForElement(content) && !content->GetProperty(nsGkAtoms::MinimalDisplayPort) && StaticPrefs::apz_paint_skipping_enabled()) { // 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 = DisplayPortUtils::GetDisplayPort(content, &displayPort); displayPort.MoveBy(-mScrolledFrame->GetPosition()); PAINT_SKIP_LOG( "New scrollpos %s usingDP %d dpEqual %d scrollableByApz " "%d perspective %d bglocal %d filter %d\n", ToString(CSSPoint::FromAppUnits(GetScrollPosition())).c_str(), usingDisplayPort, displayPort.IsEqualEdges(oldDisplayPort), mScrollableByAPZ, HasPerspective(), HasBgAttachmentLocal(), mHasOutOfFlowContentInsideFilter); if (usingDisplayPort && displayPort.IsEqualEdges(oldDisplayPort) && !HasPerspective() && !HasBgAttachmentLocal() && !mHasOutOfFlowContentInsideFilter) { bool haveScrollLinkedEffects = content->GetComposedDoc()->HasScrollLinkedEffect(); bool apzDisabled = haveScrollLinkedEffects && StaticPrefs::apz_disable_for_scroll_linked_effects(); if (!apzDisabled) { if (LastScrollOrigin() == ScrollOrigin::Apz) { schedulePaint = false; PAINT_SKIP_LOG("Skipping due to APZ scroll\n"); } else if (mScrollableByAPZ) { nsIWidget* widget = presContext->GetNearestWidget(); WindowRenderer* renderer = widget ? widget->GetWindowRenderer() : nullptr; if (renderer) { mozilla::layers::ScrollableLayerGuid::ViewID id; 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. MOZ_ASSERT(!mScrollUpdates.IsEmpty()); success = renderer->AddPendingScrollUpdateForNextTransaction( id, mScrollUpdates.LastElement()); if (success) { schedulePaint = false; SchedulePaint(nsIFrame::PAINT_COMPOSITE_ONLY); PAINT_SKIP_LOG( "Skipping due to APZ-forwarded main-thread scroll\n"); } else { PAINT_SKIP_LOG( "Failed to set pending scroll update on layer manager\n"); } } } } } } // If the new scroll offset is going to clobber APZ's scroll offset, for // the RCD-RSF this will have the effect of updating the visual viewport // offset in a way that keeps the relative offset between the layout and // visual viewports constant. This will cause APZ to send us a new visual // viewport offset, but instead of waiting for that, just set the value // we expect APZ will set ourselves, to minimize the chances of // inconsistencies from querying a stale value. if (mIsRoot && nsLayoutUtils::CanScrollOriginClobberApz(aOrigin)) { AutoWeakFrame weakFrame(this); AutoScrollbarRepaintSuppression repaintSuppression(this, weakFrame, !schedulePaint); nsPoint visualViewportOffset = curPos; if (presContext->PresShell()->IsVisualViewportOffsetSet()) { visualViewportOffset = presContext->PresShell()->GetVisualViewportOffset(); } nsPoint relativeOffset = visualViewportOffset - curPos; presContext->PresShell()->SetVisualViewportOffset(pt + relativeOffset, curPos); if (!weakFrame.IsAlive()) { return; } } if (schedulePaint) { SchedulePaint(); if (needFrameVisibilityUpdate) { presContext->PresShell()->ScheduleApproximateFrameVisibilityUpdateNow(); } } if (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. RecomputePerspectiveChildrenOverflow(this); // 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. UpdateOverflow(); } ScheduleSyntheticMouseMove(); nsAutoScriptBlocker scriptBlocker; PresShell::AutoAssertNoFlush noFlush(*PresShell()); { // scope the AutoScrollbarRepaintSuppression AutoWeakFrame weakFrame(this); AutoScrollbarRepaintSuppression repaintSuppression(this, weakFrame, !schedulePaint); UpdateScrollbarPosition(); if (!weakFrame.IsAlive()) { return; } } presContext->RecordInteractionTime( nsPresContext::InteractionType::ScrollInteraction, TimeStamp::Now()); PostScrollEvent(); // If this is a viewport scroll, this could affect the relative offset // between layout and visual viewport, so we might have to fire a visual // viewport scroll event as well. if (mIsRoot) { if (auto* window = nsGlobalWindowInner::Cast( PresContext()->Document()->GetInnerWindow())) { window->VisualViewport()->PostScrollEvent( presContext->PresShell()->GetVisualViewportOffset(), curPos); } } // Schedule the scroll-timelines linked to its scrollable frame. // if `pt == curPos`, we early return, so the position must be changed at // this moment. Therefore, we can schedule scroll animations directly. ScheduleScrollAnimations(); // notify the listeners. for (uint32_t i = 0; i < mListeners.Length(); i++) { mListeners[i]->ScrollPositionDidChange(pt.x, pt.y); } if (nsCOMPtr docShell = presContext->GetDocShell()) { docShell->NotifyScrollObservers(); } } // Finds the max z-index of the items in aList that meet the following // conditions // 1) have z-index auto or z-index >= 0, and // 2) aFrame is a proper ancestor of the item's frame. // Returns Nothing() if there is no such item. static Maybe MaxZIndexInListOfItemsContainedInFrame( nsDisplayList* aList, nsIFrame* aFrame) { Maybe maxZIndex = Nothing(); for (nsDisplayItem* item : *aList) { int32_t zIndex = item->ZIndex(); if (zIndex < 0 || !nsLayoutUtils::IsProperAncestorFrame(aFrame, item->Frame())) { continue; } if (!maxZIndex) { maxZIndex = Some(zIndex); } else { maxZIndex = Some(std::max(maxZIndex.value(), zIndex)); } } return maxZIndex; } template static void AppendInternalItemToTop(const nsDisplayListSet& aLists, T* aItem, const Maybe& aZIndex) { if (aZIndex) { aItem->SetOverrideZIndex(aZIndex.value()); aLists.PositionedDescendants()->AppendToTop(aItem); } else { aLists.Content()->AppendToTop(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 const uint32_t APPEND_OVERLAY = 0x8; static const uint32_t APPEND_TOP = 0x10; static void AppendToTop(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists, nsDisplayList* aSource, nsIFrame* aSourceFrame, nsIFrame* aScrollFrame, uint32_t aFlags) { if (aSource->IsEmpty()) { return; } nsDisplayWrapList* newItem; const ActiveScrolledRoot* asr = aBuilder->CurrentActiveScrolledRoot(); if (aFlags & APPEND_OWN_LAYER) { ScrollbarData scrollbarData; if (aFlags & APPEND_SCROLLBAR_CONTAINER) { scrollbarData = ScrollbarData::CreateForScrollbarContainer( aBuilder->GetCurrentScrollbarDirection(), aBuilder->GetCurrentScrollbarTarget()); // Direction should be set MOZ_ASSERT(scrollbarData.mDirection.isSome()); } newItem = MakeDisplayItemWithIndex( aBuilder, aSourceFrame, /* aIndex = */ nsDisplayOwnLayer::OwnLayerForScrollbar, aSource, asr, nsDisplayOwnLayerFlags::None, scrollbarData, true, false); } else { // Build the wrap list with an index of 1, since the scrollbar frame itself // might have already built an nsDisplayWrapList. newItem = MakeDisplayItemWithIndex( aBuilder, aSourceFrame, 1, aSource, asr, false); } if (!newItem) { return; } 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. Maybe zIndex = Nothing(); if (aFlags & APPEND_TOP) { zIndex = Some(INT32_MAX); } else if (aFlags & APPEND_OVERLAY) { zIndex = MaxZIndexInListOfItemsContainedInFrame( aLists.PositionedDescendants(), aScrollFrame); } else if (aSourceFrame->StylePosition()->mZIndex.IsInteger()) { zIndex = Some(aSourceFrame->StylePosition()->mZIndex.integer._0); } AppendInternalItemToTop(aLists, newItem, zIndex); } else { aLists.BorderBackground()->AppendToTop(newItem); } } struct HoveredStateComparator { static bool Hovered(const nsIFrame* aFrame) { return aFrame->GetContent()->IsElement() && aFrame->GetContent()->AsElement()->HasAttr(nsGkAtoms::hover); } bool Equals(nsIFrame* A, nsIFrame* B) const { return Hovered(A) == Hovered(B); } bool LessThan(nsIFrame* A, nsIFrame* B) const { return !Hovered(A) && Hovered(B); } }; void nsHTMLScrollFrame::AppendScrollPartsTo(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists, bool aCreateLayer, bool aPositioned) { const bool overlayScrollbars = UsesOverlayScrollbars(); AutoTArray scrollParts; for (nsIFrame* kid : 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. const mozilla::layers::ScrollableLayerGuid::ViewID scrollTargetId = IsScrollingActive() ? nsLayoutUtils::FindOrCreateIDFor(mScrolledFrame->GetContent()) : mozilla::layers::ScrollableLayerGuid::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) { nsRect scrollPartsClip(aBuilder->ToReferenceFrame(this), TrueOuterSize(aBuilder)); clipState.ClipContentDescendants(scrollPartsClip); } for (uint32_t i = 0; i < scrollParts.Length(); ++i) { MOZ_ASSERT(scrollParts[i]); Maybe scrollDirection; uint32_t appendToTopFlags = 0; if (scrollParts[i] == mVScrollbarBox) { scrollDirection.emplace(ScrollDirection::eVertical); appendToTopFlags |= APPEND_SCROLLBAR_CONTAINER; } if (scrollParts[i] == mHScrollbarBox) { MOZ_ASSERT(!scrollDirection.isSome()); scrollDirection.emplace(ScrollDirection::eHorizontal); 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. const nsRect visible = mIsRoot && PresContext()->IsRootContentDocumentCrossProcess() ? scrollParts[i]->InkOverflowRectRelativeToParent() : aBuilder->GetVisibleRect(); if (visible.IsEmpty()) { continue; } const nsRect dirty = mIsRoot && PresContext()->IsRootContentDocumentCrossProcess() ? scrollParts[i]->InkOverflowRectRelativeToParent() : aBuilder->GetDirtyRect(); // Always create layers for overlay scrollbars so that we don't create a // giant layer covering the whole scrollport if both scrollbars are visible. const bool isOverlayScrollbar = scrollDirection.isSome() && overlayScrollbars; const bool createLayer = aCreateLayer || isOverlayScrollbar || StaticPrefs::layout_scrollbars_always_layerize_track(); nsDisplayListCollection partList(aBuilder); { nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild( aBuilder, this, visible, dirty); nsDisplayListBuilder::AutoCurrentScrollbarInfoSetter infoSetter( aBuilder, scrollTargetId, scrollDirection, createLayer); BuildDisplayListForChild( aBuilder, scrollParts[i], partList, nsIFrame::DisplayChildFlag::ForceStackingContext); } // DisplayChildFlag::ForceStackingContext put everything into // partList.PositionedDescendants(). if (partList.PositionedDescendants()->IsEmpty()) { continue; } if (createLayer) { appendToTopFlags |= APPEND_OWN_LAYER; } if (aPositioned) { appendToTopFlags |= APPEND_POSITIONED; } if (isOverlayScrollbar || scrollParts[i] == mResizerBox) { if (isOverlayScrollbar && mIsRoot) { appendToTopFlags |= APPEND_TOP; } else { appendToTopFlags |= APPEND_OVERLAY; aBuilder->SetDisablePartialUpdates(true); } } { nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild( aBuilder, scrollParts[i], visible + GetOffsetTo(scrollParts[i]), dirty + GetOffsetTo(scrollParts[i])); if (scrollParts[i]->IsTransformed()) { nsPoint toOuterReferenceFrame; const nsIFrame* outerReferenceFrame = aBuilder->FindReferenceFrameFor( scrollParts[i]->GetParent(), &toOuterReferenceFrame); toOuterReferenceFrame += scrollParts[i]->GetPosition(); buildingForChild.SetReferenceFrameAndCurrentOffset( outerReferenceFrame, toOuterReferenceFrame); } nsDisplayListBuilder::AutoCurrentScrollbarInfoSetter infoSetter( aBuilder, scrollTargetId, scrollDirection, createLayer); ::AppendToTop(aBuilder, aLists, partList.PositionedDescendants(), scrollParts[i], this, appendToTopFlags); } } } nsRect nsHTMLScrollFrame::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 = GetLayoutScrollRange(); nsPoint scrollPos = GetScrollPosition(); nsMargin expand(0, 0, 0, 0); nscoord vertShift = StaticPrefs::layout_framevisibility_numscrollportheights() * aRect.height; if (scrollRange.y < scrollPos.y) { expand.top = vertShift; } if (scrollPos.y < scrollRange.YMost()) { expand.bottom = vertShift; } nscoord horzShift = StaticPrefs::layout_framevisibility_numscrollportwidths() * 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 DisplayItemClipChain* aExtraClip, nsTHashMap, const DisplayItemClipChain*>& aCache) { for (nsDisplayItem* i : *aList) { if (!ShouldBeClippedByFrame(aClipFrame, i->Frame())) { continue; } const DisplayItemType type = i->GetType(); if (type != DisplayItemType::TYPE_CARET && type != DisplayItemType::TYPE_CONTAINER) { const DisplayItemClipChain* clip = i->GetClipChain(); const DisplayItemClipChain* intersection = nullptr; if (aCache.Get(clip, &intersection)) { i->SetClipChain(intersection, true); } else { i->IntersectClip(aBuilder, aExtraClip, true); aCache.InsertOrUpdate(clip, i->GetClipChain()); } } nsDisplayList* children = i->GetSameCoordinateSystemChildren(); if (children) { ClipItemsExceptCaret(children, aBuilder, aClipFrame, aExtraClip, aCache); } } } static void ClipListsExceptCaret(nsDisplayListCollection* aLists, nsDisplayListBuilder* aBuilder, nsIFrame* aClipFrame, const DisplayItemClipChain* aExtraClip) { nsTHashMap, const DisplayItemClipChain*> cache; ClipItemsExceptCaret(aLists->BorderBackground(), aBuilder, aClipFrame, aExtraClip, cache); ClipItemsExceptCaret(aLists->BlockBorderBackgrounds(), aBuilder, aClipFrame, aExtraClip, cache); ClipItemsExceptCaret(aLists->Floats(), aBuilder, aClipFrame, aExtraClip, cache); ClipItemsExceptCaret(aLists->PositionedDescendants(), aBuilder, aClipFrame, aExtraClip, cache); ClipItemsExceptCaret(aLists->Outlines(), aBuilder, aClipFrame, aExtraClip, cache); ClipItemsExceptCaret(aLists->Content(), aBuilder, aClipFrame, aExtraClip, cache); } // This is similar to a "save-restore" RAII class for // DisplayListBuilder::ContainsBlendMode(), with a slight enhancement. // If this class is put on the stack and then unwound, the DL builder's // ContainsBlendMode flag will be effectively the same as if this class wasn't // put on the stack. However, if the CaptureContainsBlendMode method is called, // there will be a difference - the blend mode in the descendant display lists // will be "captured" and extracted. // The main goal here is to allow conditionally capturing the flag that // indicates whether or not a blend mode was encountered in the descendant part // of the display list. class MOZ_RAII AutoContainsBlendModeCapturer { nsDisplayListBuilder& mBuilder; bool mSavedContainsBlendMode; public: explicit AutoContainsBlendModeCapturer(nsDisplayListBuilder& aBuilder) : mBuilder(aBuilder), mSavedContainsBlendMode(aBuilder.ContainsBlendMode()) { mBuilder.SetContainsBlendMode(false); } bool CaptureContainsBlendMode() { // "Capture" the flag by extracting and clearing the ContainsBlendMode flag // on the builder. bool capturedBlendMode = mBuilder.ContainsBlendMode(); mBuilder.SetContainsBlendMode(false); return capturedBlendMode; } ~AutoContainsBlendModeCapturer() { // If CaptureContainsBlendMode() was called, the descendant blend mode was // "captured" and so uncapturedContainsBlendMode will be false. If // CaptureContainsBlendMode() wasn't called, then no capture occurred, and // uncapturedContainsBlendMode may be true if there was a descendant blend // mode. In that case, we set the flag on the DL builder so that we restore // state to what it would have been without this RAII class on the stack. bool uncapturedContainsBlendMode = mBuilder.ContainsBlendMode(); mBuilder.SetContainsBlendMode(mSavedContainsBlendMode || uncapturedContainsBlendMode); } }; void nsHTMLScrollFrame::MaybeCreateTopLayerAndWrapRootItems( nsDisplayListBuilder* aBuilder, nsDisplayListCollection& aSet, bool aCreateAsyncZoom, AutoContainsBlendModeCapturer* aAsyncZoomBlendCapture, const nsRect& aAsyncZoomClipRect, nscoord* aRadii) { if (!mIsRoot) { return; } // Create any required items for the 'top layer' and check if they'll be // opaque over the entire area of the viewport. If they are, then we can // skip building display items for the rest of the page. if (ViewportFrame* viewport = do_QueryFrame(GetParent())) { bool topLayerIsOpaque = false; if (nsDisplayWrapList* topLayerWrapList = viewport->BuildDisplayListForTopLayer(aBuilder, &topLayerIsOpaque)) { // If the top layer content is opaque, and we're the root content document // in the process, we can drop the display items behind it. We only // support doing this for the root content document in the process, since // the top layer content might have fixed position items that have a // scrolltarget referencing the APZ data for the document. APZ builds this // data implicitly for the root content document in the process, but // subdocuments etc need their display items to generate it, so we can't // cull those. if (topLayerIsOpaque && PresContext()->IsRootContentDocumentInProcess()) { aSet.DeleteAll(aBuilder); } aSet.PositionedDescendants()->AppendToTop(topLayerWrapList); } } nsDisplayList rootResultList(aBuilder); bool serializedList = false; auto SerializeList = [&] { if (!serializedList) { serializedList = true; aSet.SerializeWithCorrectZOrder(&rootResultList, GetContent()); } }; if (nsIFrame* rootStyleFrame = GetFrameForStyle()) { bool usingBackdropFilter = rootStyleFrame->StyleEffects()->HasBackdropFilters() && rootStyleFrame->IsVisibleForPainting(); if (rootStyleFrame->StyleEffects()->HasFilters() && !aBuilder->IsForGenerateGlyphMask()) { SerializeList(); rootResultList.AppendNewToTop( aBuilder, this, &rootResultList, rootStyleFrame, usingBackdropFilter); } if (usingBackdropFilter) { SerializeList(); DisplayListClipState::AutoSaveRestore clipState(aBuilder); nsRect backdropRect = GetRectRelativeToSelf() + aBuilder->ToReferenceFrame(this); rootResultList.AppendNewToTop( aBuilder, this, &rootResultList, backdropRect, rootStyleFrame); } } if (aCreateAsyncZoom) { MOZ_ASSERT(mIsRoot); // Wrap all our scrolled contents in an nsDisplayAsyncZoom. This will be // the layer that gets scaled for APZ zooming. It does not have the // scrolled ASR, but it does have the composition bounds clip applied to // it. The children have the layout viewport clip applied to them (above). // Effectively we are double clipping to the viewport, at potentially // different async scales. SerializeList(); if (aAsyncZoomBlendCapture->CaptureContainsBlendMode()) { // The async zoom contents contain a mix-blend mode, so let's wrap all // those contents into a blend container, and then wrap the blend // container in the async zoom container. Otherwise the blend container // ends up outside the zoom container which results in blend failure for // WebRender. nsDisplayItem* blendContainer = nsDisplayBlendContainer::CreateForMixBlendMode( aBuilder, this, &rootResultList, aBuilder->CurrentActiveScrolledRoot()); rootResultList.AppendToTop(blendContainer); // Blend containers can be created or omitted during partial updates // depending on the dirty rect. So we basically can't do partial updates // if there's a blend container involved. There is equivalent code to this // in the BuildDisplayListForStackingContext function as well, with a more // detailed comment explaining things better. if (aBuilder->IsRetainingDisplayList()) { if (aBuilder->IsPartialUpdate()) { aBuilder->SetPartialBuildFailed(true); } else { aBuilder->SetDisablePartialUpdates(true); } } } mozilla::layers::FrameMetrics::ViewID viewID = nsLayoutUtils::FindOrCreateIDFor(mScrolledFrame->GetContent()); DisplayListClipState::AutoSaveRestore clipState(aBuilder); clipState.ClipContentDescendants(aAsyncZoomClipRect, aRadii); rootResultList.AppendNewToTop( aBuilder, this, &rootResultList, aBuilder->CurrentActiveScrolledRoot(), viewID); } if (serializedList) { aSet.Content()->AppendToTop(&rootResultList); } } void nsHTMLScrollFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { SetAndNullOnExit tmpBuilder( mReferenceFrameDuringPainting, aBuilder->GetCurrentReferenceFrame()); if (aBuilder->IsForFrameVisibility()) { NotifyApproximateFrameVisibilityUpdate(false); } DisplayBorderBackgroundOutline(aBuilder, aLists); const bool isRootContent = mIsRoot && PresContext()->IsRootContentDocumentCrossProcess(); nsRect effectiveScrollPort = mScrollPort; if (isRootContent && PresContext()->HasDynamicToolbar()) { // Expand the scroll port to the size including the area covered by dynamic // toolbar in the case where the dynamic toolbar is being used since // position:fixed elements attached to this root scroller might be taller // than its scroll port (e.g 100vh). Even if the dynamic toolbar covers the // taller area, it doesn't mean the area is clipped by the toolbar because // the dynamic toolbar is laid out outside of our topmost window and it // transitions without changing our topmost window size. effectiveScrollPort.SizeTo(nsLayoutUtils::ExpandHeightForDynamicToolbar( PresContext(), effectiveScrollPort.Size())); } // 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. const bool ignoringThisScrollFrame = aBuilder->GetIgnoreScrollFrame() == this; // 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 visibleRect = aBuilder->GetVisibleRect(); nsRect dirtyRect = aBuilder->GetDirtyRect(); if (!ignoringThisScrollFrame) { visibleRect = visibleRect.Intersect(effectiveScrollPort); dirtyRect = dirtyRect.Intersect(effectiveScrollPort); } bool dirtyRectHasBeenOverriden = false; Unused << DecideScrollableLayer(aBuilder, &visibleRect, &dirtyRect, /* aSetBase = */ !mIsRoot, &dirtyRectHasBeenOverriden); 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); visibleRect = 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. const bool createLayersForScrollbars = isRootContent; nsDisplayListCollection set(aBuilder); if (ignoringThisScrollFrame) { // 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 && mWillBuildScrollableLayer && aBuilder->IsPaintingToWindow(); if (addScrollBars) { // Add classic scrollbars. AppendScrollPartsTo(aBuilder, set, createLayersForScrollbars, false); } { nsDisplayListBuilder::AutoBuildingDisplayList building( aBuilder, this, visibleRect, dirtyRect); // 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. BuildDisplayListForChild(aBuilder, mScrolledFrame, set); } MaybeCreateTopLayerAndWrapRootItems(aBuilder, set, /* aCreateAsyncZoom = */ false, nullptr, nsRect(), nullptr); if (addScrollBars) { // Add overlay scrollbars. AppendScrollPartsTo(aBuilder, set, createLayersForScrollbars, true); } set.MoveTo(aLists); return; } // 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. const bool couldBuildLayer = [&] { if (!aBuilder->IsPaintingToWindow()) { return false; } if (mWillBuildScrollableLayer) { return true; } return StyleVisibility()->IsVisible() && nsLayoutUtils::AsyncPanZoomEnabled(this) && WantAsyncScroll(); }(); // 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, aLists, createLayersForScrollbars, false); const nsStyleDisplay* disp = StyleDisplay(); if (aBuilder->IsForPainting() && disp->mWillChange.bits & StyleWillChangeBits::SCROLL) { aBuilder->AddToWillChangeBudget(this, GetVisualViewportSize()); } mScrollParentID = aBuilder->GetCurrentScrollParentId(); Maybe contentBoxClip; Maybe extraContentBoxClipForNonCaretContent; if (MOZ_UNLIKELY( disp->mOverflowClipBoxBlock == StyleOverflowClipBox::ContentBox || disp->mOverflowClipBoxInline == StyleOverflowClipBox::ContentBox)) { WritingMode wm = mScrolledFrame->GetWritingMode(); bool cbH = (wm.IsVertical() ? disp->mOverflowClipBoxBlock : disp->mOverflowClipBoxInline) == StyleOverflowClipBox::ContentBox; bool cbV = (wm.IsVertical() ? disp->mOverflowClipBoxInline : disp->mOverflowClipBoxBlock) == StyleOverflowClipBox::ContentBox; // We only clip if there is *scrollable* overflow, to avoid clipping // *ink* overflow unnecessarily. nsRect clipRect = effectiveScrollPort + aBuilder->ToReferenceFrame(this); nsMargin padding = GetUsedPadding(); if (!cbH) { padding.left = padding.right = nscoord(0); } if (!cbV) { padding.top = padding.bottom = nscoord(0); } clipRect.Deflate(padding); nsRect so = mScrolledFrame->ScrollableOverflowRect(); if ((cbH && (clipRect.width != so.width || so.x < 0)) || (cbV && (clipRect.height != so.height || so.y < 0))) { // The non-inflated clip needs to be set on all non-caret items. // We prepare an extra DisplayItemClipChain here that will be intersected // with those items after they've been created. const ActiveScrolledRoot* asr = aBuilder->CurrentActiveScrolledRoot(); DisplayItemClip newClip; newClip.SetTo(clipRect); const DisplayItemClipChain* extraClip = aBuilder->AllocateDisplayItemClipChain(newClip, asr, nullptr); extraContentBoxClipForNonCaretContent = Some(extraClip); nsIFrame* caretFrame = aBuilder->GetCaretFrame(); // Avoid clipping it in a zero-height line box (heuristic only). if (caretFrame && 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. nsRect inflatedClip = clipRect; inflatedClip.Inflate( nsMargin(caretRect.height / 2, caretRect.width, 0, 0)); contentBoxClip = Some(inflatedClip); } } } AutoContainsBlendModeCapturer blendCapture(*aBuilder); const bool willBuildAsyncZoomContainer = mWillBuildScrollableLayer && aBuilder->ShouldBuildAsyncZoomContainer() && isRootContent; nsRect scrollPortClip = effectiveScrollPort + aBuilder->ToReferenceFrame(this); nsRect clipRect = scrollPortClip; // Our override of GetBorderRadii ensures we never have a radius at // the corners where we have a scrollbar. nscoord radii[8]; const bool haveRadii = GetPaddingBoxBorderRadii(radii); if (mIsRoot) { clipRect.SizeTo(nsLayoutUtils::CalculateCompositionSizeForFrame( this, true /* aSubtractScrollbars */, nullptr /* aOverrideScrollPortSize */, // With the dynamic toolbar, this CalculateCompositionSizeForFrame call // basically expands the region being covered up by the dynamic toolbar, // but if the root scroll container is not scrollable, e.g. the root // element has `overflow: hidden` or `position: fixed`, the function // doesn't expand the region since expanding the region in such cases // will prevent the content from restoring zooming to 1.0 zoom level // such as bug 1652190. That said, this `clipRect` which will be used // for the async zoom container needs to be expanded because zoomed-in // contents can be scrollable __visually__ so that the region under the // dynamic toolbar area will be revealed. nsLayoutUtils::IncludeDynamicToolbar::Force)); // The composition size is essentially in visual coordinates. // If we are hit-testing in layout coordinates, transform the clip rect // to layout coordinates to match. if (aBuilder->IsRelativeToLayoutViewport() && isRootContent) { clipRect = ViewportUtils::VisualToLayout(clipRect, PresShell()); } } { DisplayListClipState::AutoSaveRestore clipState(aBuilder); // If we're building an async zoom container, clip the contents inside // to the layout viewport (scrollPortClip). The composition bounds clip // (clipRect) will be applied to the zoom container itself in // MaybeCreateTopLayerAndWrapRootItems. nsRect clipRectForContents = willBuildAsyncZoomContainer ? scrollPortClip : clipRect; if (mIsRoot) { clipState.ClipContentDescendants(clipRectForContents, haveRadii ? radii : nullptr); } else { clipState.ClipContainingBlockDescendants(clipRectForContents, haveRadii ? radii : nullptr); } Maybe contentBoxClipState; if (contentBoxClip) { contentBoxClipState.emplace(aBuilder); if (mIsRoot) { contentBoxClipState->ClipContentDescendants(*contentBoxClip); } else { contentBoxClipState->ClipContainingBlockDescendants(*contentBoxClip); } } nsDisplayListBuilder::AutoCurrentActiveScrolledRootSetter asrSetter( aBuilder); if (mWillBuildScrollableLayer && aBuilder->IsPaintingToWindow()) { // If this scroll frame has a first scrollable frame sequence number, // ensure that it matches the current paint sequence number. If it does // not, reset it so that we can expire the displayport. The stored // sequence number will not match that of the current paint if the dom // was mutated in some way that alters the order of scroll frames. if (IsFirstScrollableFrameSequenceNumber().isSome() && *IsFirstScrollableFrameSequenceNumber() != nsDisplayListBuilder::GetPaintSequenceNumber()) { SetIsFirstScrollableFrameSequenceNumber(Nothing()); } asrSetter.EnterScrollFrame(this); } if (couldBuildLayer && mScrolledFrame->GetContent()) { asrSetter.SetCurrentScrollParentId( nsLayoutUtils::FindOrCreateIDFor(mScrolledFrame->GetContent())); } if (mWillBuildScrollableLayer && aBuilder->BuildCompositorHitTestInfo()) { // Create a hit test info item for the scrolled content that's not // clipped to the displayport. This ensures that within the bounds // of the scroll frame, the scrolled content is always hit, even // if we are checkerboarding. CompositorHitTestInfo info = mScrolledFrame->GetCompositorHitTestInfo(aBuilder); if (info != CompositorHitTestInvisibleToHit) { auto* hitInfo = MakeDisplayItemWithIndex( aBuilder, mScrolledFrame, 1); if (hitInfo) { aBuilder->SetCompositorHitTestInfo(info); set.BorderBackground()->AppendToTop(hitInfo); } } } { // 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->ScrollableOverflowRect(), mScrollPort.Size()) + mScrolledFrame->GetPosition(); bool clippedToDisplayPort = false; if (mWillBuildScrollableLayer && aBuilder->IsPaintingToWindow()) { // 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(visibleRect); clippedToDisplayPort = scrolledRectClip.IsEqualEdges(visibleRect); } scrolledRectClipState.ClipContainingBlockDescendants( scrolledRectClip + aBuilder->ToReferenceFrame(this)); if (clippedToDisplayPort) { // We have to do this after the ClipContainingBlockDescendants call // above, otherwise that call will clobber the flag set by this call // to SetClippedToDisplayPort. scrolledRectClipState.SetClippedToDisplayPort(); } nsRect visibleRectForChildren = visibleRect; nsRect dirtyRectForChildren = dirtyRect; // If we are entering the RCD-RSF, we are crossing the async zoom // container boundary, and need to convert from visual to layout // coordinates. if (willBuildAsyncZoomContainer && aBuilder->IsForEventDelivery()) { MOZ_ASSERT(ViewportUtils::IsZoomedContentRoot(mScrolledFrame)); visibleRectForChildren = ViewportUtils::VisualToLayout(visibleRectForChildren, PresShell()); dirtyRectForChildren = ViewportUtils::VisualToLayout(dirtyRectForChildren, PresShell()); } nsDisplayListBuilder::AutoBuildingDisplayList building( aBuilder, this, visibleRectForChildren, dirtyRectForChildren); BuildDisplayListForChild(aBuilder, mScrolledFrame, set); if (dirtyRectHasBeenOverriden && StaticPrefs::layout_display_list_show_rebuild_area()) { nsDisplaySolidColor* color = MakeDisplayItem( aBuilder, this, dirtyRect + aBuilder->GetCurrentFrameOffsetToReferenceFrame(), NS_RGBA(0, 0, 255, 64), false); if (color) { color->SetOverrideZIndex(INT32_MAX); set.PositionedDescendants()->AppendToTop(color); } } } if (extraContentBoxClipForNonCaretContent) { // The items were built while the inflated content box clip was in // effect, so that the caret wasn't clipped unnecessarily. We apply // the non-inflated clip to the non-caret items now, by intersecting // it with their existing clip. ClipListsExceptCaret(&set, aBuilder, mScrolledFrame, *extraContentBoxClipForNonCaretContent); } if (aBuilder->IsPaintingToWindow()) { mIsParentToActiveScrollFrames = ShouldActivateAllScrollFrames() ? asrSetter.GetContainsNonMinimalDisplayPort() : asrSetter.ShouldForceLayerForScrollParent(); } if (asrSetter.ShouldForceLayerForScrollParent()) { // 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 asrSetter variable is created). MOZ_ASSERT(couldBuildLayer && mScrolledFrame->GetContent() && aBuilder->IsPaintingToWindow()); if (!mWillBuildScrollableLayer) { // Set a displayport so next paint we don't have to force layerization // after the fact. It's ok to pass DoNotRepaint here, since we've // already painted the change and we're just optimizing it to be // detected earlier. We also won't confuse RetainedDisplayLists // with the silent change, since we explicitly request partial updates // to be disabled on the next paint. DisplayPortUtils::SetDisplayPortMargins( GetContent(), PresShell(), DisplayPortMargins::Empty(GetContent()), DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes, 0, DisplayPortUtils::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; nsRect copyOfVisibleRect = visibleRect; Unused << DecideScrollableLayer(aBuilder, ©OfVisibleRect, ©OfDirtyRect, /* aSetBase = */ false, nullptr); if (mWillBuildScrollableLayer) { #ifndef MOZ_WIDGET_ANDROID gfxCriticalNoteOnce << "inserted scroll frame"; #endif asrSetter.InsertScrollFrame(this); aBuilder->SetDisablePartialUpdates(true); } } } } if (mWillBuildScrollableLayer && aBuilder->IsPaintingToWindow()) { aBuilder->ForceLayerForScrollParent(); } MaybeCreateTopLayerAndWrapRootItems( aBuilder, set, willBuildAsyncZoomContainer, &blendCapture, clipRect, haveRadii ? radii : nullptr); // We want to call SetContainsNonMinimalDisplayPort if // mWillBuildScrollableLayer is true for any reason other than having a // minimal display port. if (aBuilder->IsPaintingToWindow()) { if (DisplayPortUtils::HasNonMinimalDisplayPort(GetContent()) || mZoomableByAPZ || nsContentUtils::HasScrollgrab(GetContent())) { aBuilder->SetContainsNonMinimalDisplayPort(); } } if (couldBuildLayer) { CompositorHitTestInfo info(CompositorHitTestFlags::eVisibleToHitTest, CompositorHitTestFlags::eInactiveScrollframe); // If the scroll frame has non-default overscroll-behavior, instruct // APZ to require a target confirmation before processing events that // hit this scroll frame (that is, to drop the events if a // confirmation does not arrive within the timeout period). Otherwise, // APZ's fallback behaviour of scrolling the enclosing scroll frame // would violate the specified overscroll-behavior. auto overscroll = GetOverscrollBehaviorInfo(); if (overscroll.mBehaviorX != OverscrollBehavior::Auto || overscroll.mBehaviorY != OverscrollBehavior::Auto) { info += CompositorHitTestFlags::eRequiresTargetConfirmation; } nsRect area = effectiveScrollPort + aBuilder->ToReferenceFrame(this); // 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. if (!mWillBuildScrollableLayer && aBuilder->BuildCompositorHitTestInfo()) { // Make sure the z-index of the inactive item is at least zero. // Otherwise, it will end up behind non-positioned items in the scrolled // content. int32_t zIndex = MaxZIndexInListOfItemsContainedInFrame( set.PositionedDescendants(), this) .valueOr(0); if (aBuilder->IsPartialUpdate()) { for (nsDisplayItem* item : mScrolledFrame->DisplayItems()) { if (item->GetType() == DisplayItemType::TYPE_COMPOSITOR_HITTEST_INFO) { auto* hitTestItem = static_cast(item); if (hitTestItem->GetHitTestInfo().Info().contains( CompositorHitTestFlags::eInactiveScrollframe)) { zIndex = std::max(zIndex, hitTestItem->ZIndex()); item->SetCantBeReused(); } } } } nsDisplayCompositorHitTestInfo* hitInfo = MakeDisplayItemWithIndex( aBuilder, mScrolledFrame, 1, area, info); if (hitInfo) { AppendInternalItemToTop(set, hitInfo, Some(zIndex)); aBuilder->SetCompositorHitTestInfo(info); } } if (aBuilder->ShouldBuildScrollInfoItemsForHoisting()) { aBuilder->AppendNewScrollInfoItemForHoisting( MakeDisplayItem(aBuilder, mScrolledFrame, this, info, area)); } } // Now display overlay scrollbars and the resizer, if we have one. AppendScrollPartsTo(aBuilder, set, createLayersForScrollbars, true); set.MoveTo(aLists); } nsRect nsHTMLScrollFrame::RestrictToRootDisplayPort( const nsRect& aDisplayportBase) { // This function clips aDisplayportBase so that it is no larger than the // root frame's displayport (or the root composition bounds, if we can't // obtain the root frame's displayport). This is useful for ensuring that // the displayport of a tall scrollframe doesn't gobble up all the memory. nsPresContext* pc = PresContext(); const nsPresContext* rootPresContext = pc->GetInProcessRootContentDocumentPresContext(); if (!rootPresContext) { rootPresContext = pc->GetRootPresContext(); } if (!rootPresContext) { return aDisplayportBase; } const mozilla::PresShell* const rootPresShell = rootPresContext->PresShell(); nsIFrame* rootFrame = rootPresShell->GetRootScrollFrame(); if (!rootFrame) { rootFrame = rootPresShell->GetRootFrame(); } if (!rootFrame) { return aDisplayportBase; } // Make sure we aren't trying to restrict to our own displayport, which is a // circular dependency. MOZ_ASSERT(!mIsRoot || rootPresContext != pc); nsRect rootDisplayPort; bool hasDisplayPort = rootFrame->GetContent() && DisplayPortUtils::GetDisplayPort( rootFrame->GetContent(), &rootDisplayPort); if (hasDisplayPort) { // The display port of the root frame already factors in it's callback // transform, so subtract it out here, the GetCumulativeApzCallbackTransform // call below will add it back. MOZ_LOG(sDisplayportLog, LogLevel::Verbose, ("RestrictToRootDisplayPort: Existing root displayport is %s\n", ToString(rootDisplayPort).c_str())); if (nsIContent* content = rootFrame->GetContent()) { if (void* property = content->GetProperty(nsGkAtoms::apzCallbackTransform)) { rootDisplayPort -= CSSPoint::ToAppUnits(*static_cast(property)); } } } else { // If we don't have a display port on the root frame let's fall back to // the root composition bounds instead. 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->IsRootContentDocumentCrossProcess() && rootFrame == rootPresShell->GetRootScrollFrame()) { MOZ_LOG( sDisplayportLog, LogLevel::Verbose, ("RestrictToRootDisplayPort: Removing resolution %f from root " "composition bounds %s\n", rootPresShell->GetResolution(), ToString(rootCompBounds).c_str())); rootCompBounds = rootCompBounds.RemoveResolution(rootPresShell->GetResolution()); } rootDisplayPort = rootCompBounds; } MOZ_LOG(sDisplayportLog, LogLevel::Verbose, ("RestrictToRootDisplayPort: Intermediate root displayport %s\n", ToString(rootDisplayPort).c_str())); // We want to convert the root display port from the // coordinate space of |rootFrame| to the coordinate space of // |this|. We do that with the TransformRect call below. // However, since we care about the root display port // 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, this, rootDisplayPort); MOZ_LOG(sDisplayportLog, LogLevel::Verbose, ("RestrictToRootDisplayPort: Transformed root displayport %s\n", ToString(rootDisplayPort).c_str())); rootDisplayPort += CSSPoint::ToAppUnits( nsLayoutUtils::GetCumulativeApzCallbackTransform(this)); MOZ_LOG(sDisplayportLog, LogLevel::Verbose, ("RestrictToRootDisplayPort: Final root displayport %s\n", ToString(rootDisplayPort).c_str())); // We want to limit aDisplayportBase to be no larger than // rootDisplayPort on either axis, but we don't want to just // blindly intersect the two, because rootDisplayPort might be // offset from where aDisplayportBase is (see bug 1327095 comment // 8). Instead, we translate rootDisplayPort so as to maximize the // overlap with aDisplayportBase, and *then* do the intersection. if (rootDisplayPort.x > aDisplayportBase.x && rootDisplayPort.XMost() > aDisplayportBase.XMost()) { // rootDisplayPort is at a greater x-position for both left and // right, so translate it such that the XMost() values are the // same. This will line up the right edge of the two rects, and // might mean that rootDisplayPort.x is smaller than // aDisplayportBase.x. We can avoid that by taking the min of the // x delta and XMost() delta, but it doesn't really matter // because the intersection between the two rects below will end // up the same. rootDisplayPort.x -= (rootDisplayPort.XMost() - aDisplayportBase.XMost()); } else if (rootDisplayPort.x < aDisplayportBase.x && rootDisplayPort.XMost() < aDisplayportBase.XMost()) { // Analaogous code for when the rootDisplayPort is at a smaller // x-position. rootDisplayPort.x = aDisplayportBase.x; } // Do the same for y-axis if (rootDisplayPort.y > aDisplayportBase.y && rootDisplayPort.YMost() > aDisplayportBase.YMost()) { rootDisplayPort.y -= (rootDisplayPort.YMost() - aDisplayportBase.YMost()); } else if (rootDisplayPort.y < aDisplayportBase.y && rootDisplayPort.YMost() < aDisplayportBase.YMost()) { rootDisplayPort.y = aDisplayportBase.y; } MOZ_LOG( sDisplayportLog, LogLevel::Verbose, ("RestrictToRootDisplayPort: Root displayport translated to %s to " "better enclose %s\n", ToString(rootDisplayPort).c_str(), ToString(aDisplayportBase).c_str())); // Now we can do the intersection return aDisplayportBase.Intersect(rootDisplayPort); } /* static */ bool nsHTMLScrollFrame::ShouldActivateAllScrollFrames() { return (StaticPrefs::apz_wr_activate_all_scroll_frames() || (StaticPrefs::apz_wr_activate_all_scroll_frames_when_fission() && FissionAutostart())); } bool nsHTMLScrollFrame::DecideScrollableLayer( nsDisplayListBuilder* aBuilder, nsRect* aVisibleRect, nsRect* aDirtyRect, bool aSetBase, bool* aDirtyRectHasBeenOverriden) { nsIContent* content = GetContent(); bool hasDisplayPort = DisplayPortUtils::HasDisplayPort(content); // For hit testing purposes with fission we want to create a // minimal display port for every scroll frame that could be active. (We only // do this when aSetBase is true because we only want to do this the first // time this function is called for the same scroll frame.) if (aSetBase && !hasDisplayPort && aBuilder->IsPaintingToWindow() && ShouldActivateAllScrollFrames() && nsLayoutUtils::AsyncPanZoomEnabled(this) && WantAsyncScroll()) { // SetDisplayPortMargins calls TriggerDisplayPortExpiration which starts a // display port expiry timer for display ports that do expire. However // minimal display ports do not expire, so the display port has to be // marked before the SetDisplayPortMargins call so the expiry timer // doesn't get started. content->SetProperty(nsGkAtoms::MinimalDisplayPort, reinterpret_cast(true)); DisplayPortUtils::SetDisplayPortMargins( content, PresShell(), DisplayPortMargins::Empty(content), DisplayPortUtils::ClearMinimalDisplayPortProperty::No, 0, DisplayPortUtils::RepaintMode::DoNotRepaint); hasDisplayPort = true; } if (aBuilder->IsPaintingToWindow()) { if (aSetBase) { nsRect displayportBase = *aVisibleRect; nsPresContext* pc = PresContext(); bool isChromeRootDoc = !pc->Document()->IsContentDocument() && !pc->GetParentPresContext(); if (mIsRoot && (pc->IsRootContentDocumentCrossProcess() || isChromeRootDoc)) { displayportBase = nsRect(nsPoint(0, 0), nsLayoutUtils::CalculateCompositionSizeForFrame(this)); } else { // Make the displayport base equal to the visible rect restricted to // the scrollport and the root composition bounds, relative to the // scrollport. displayportBase = aVisibleRect->Intersect(mScrollPort); mozilla::layers::ScrollableLayerGuid::ViewID viewID = mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID; if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Verbose)) { nsLayoutUtils::FindIDFor(GetContent(), &viewID); MOZ_LOG( sDisplayportLog, LogLevel::Verbose, ("Scroll id %" PRIu64 " has visible rect %s, scroll port %s\n", viewID, ToString(*aVisibleRect).c_str(), ToString(mScrollPort).c_str())); } // Only restrict to the root displayport bounds if necessary, // as the required coordinate transformation is expensive. // And don't call RestrictToRootDisplayPort if we would be trying to // restrict to our own display port, which doesn't make sense (ie if we // are a root scroll frame in a process root prescontext). if (hasDisplayPort && (!mIsRoot || pc->GetParentPresContext()) && !DisplayPortUtils::WillUseEmptyDisplayPortMargins(content)) { displayportBase = RestrictToRootDisplayPort(displayportBase); MOZ_LOG(sDisplayportLog, LogLevel::Verbose, ("Scroll id %" PRIu64 " has restricted base %s\n", viewID, ToString(displayportBase).c_str())); } displayportBase -= mScrollPort.TopLeft(); } DisplayPortUtils::SetDisplayPortBase(GetContent(), displayportBase); } // If we don't have aSetBase == true then should have already // been called with aSetBase == true which should have set a // displayport base. MOZ_ASSERT(content->GetProperty(nsGkAtoms::DisplayPortBase)); nsRect displayPort; hasDisplayPort = DisplayPortUtils::GetDisplayPort( content, &displayPort, DisplayPortOptions().With(DisplayportRelativeTo::ScrollFrame)); auto OverrideDirtyRect = [&](const nsRect& aRect) { *aDirtyRect = aRect; if (aDirtyRectHasBeenOverriden) { *aDirtyRectHasBeenOverriden = true; } }; if (hasDisplayPort) { // Override the dirty rectangle if the displayport has been set. *aVisibleRect = displayPort; if (aBuilder->IsReusingStackingContextItems() || !aBuilder->IsPartialUpdate() || aBuilder->InInvalidSubtree() || IsFrameModified()) { OverrideDirtyRect(displayPort); } else if (HasOverrideDirtyRegion()) { nsRect* rect = GetProperty( nsDisplayListBuilder::DisplayListBuildingDisplayPortRect()); if (rect) { OverrideDirtyRect(*rect); } } } 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. auto* presShell = PresShell(); *aVisibleRect = aVisibleRect->RemoveResolution(presShell->GetResolution()); *aDirtyRect = aDirtyRect->RemoveResolution(presShell->GetResolution()); } } // 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 = hasDisplayPort || nsContentUtils::HasScrollgrab(content) || mZoomableByAPZ; return mWillBuildScrollableLayer; } void nsHTMLScrollFrame::NotifyApzTransaction() { mAllowScrollOriginDowngrade = true; mApzScrollPos = GetScrollPosition(); mApzAnimationRequested = IsLastScrollUpdateAnimating(); mApzAnimationTriggeredByScriptRequested = IsLastScrollUpdateTriggeredByScriptAnimating(); mScrollUpdates.Clear(); if (mIsRoot) { PresShell()->SetResolutionUpdated(false); } } Maybe nsHTMLScrollFrame::ComputeScrollMetadata( WebRenderLayerManager* aLayerManager, const nsIFrame* aItemFrame, const nsPoint& aOffsetToReferenceFrame) const { if (!mWillBuildScrollableLayer) { return Nothing(); } bool isRootContent = mIsRoot && PresContext()->IsRootContentDocumentCrossProcess(); MOZ_ASSERT(mScrolledFrame->GetContent()); return Some(nsLayoutUtils::ComputeScrollMetadata( mScrolledFrame, this, GetContent(), aItemFrame, aOffsetToReferenceFrame, aLayerManager, mScrollParentID, mScrollPort.Size(), isRootContent)); } bool nsHTMLScrollFrame::IsRectNearlyVisible(const nsRect& aRect) const { // Use the right rect depending on if a display port is set. nsRect displayPort; bool usingDisplayport = DisplayPortUtils::GetDisplayPort( GetContent(), &displayPort, DisplayPortOptions().With(DisplayportRelativeTo::ScrollFrame)); if (mIsRoot && !usingDisplayport && PresContext()->IsRootContentDocumentInProcess() && !PresContext()->IsRootContentDocumentCrossProcess()) { // In the case of the root scroller of OOP iframes, there are cases where // any display port value isn't set, e.g. the iframe element is out of view // in the parent document. In such cases we'd consider the iframe is not // visible. return false; } return aRect.Intersects( ExpandRectToNearlyVisible(usingDisplayport ? displayPort : mScrollPort)); } OverscrollBehaviorInfo nsHTMLScrollFrame::GetOverscrollBehaviorInfo() const { nsIFrame* frame = GetFrameForStyle(); if (!frame) { return {}; } auto& disp = *frame->StyleDisplay(); return OverscrollBehaviorInfo::FromStyleConstants(disp.mOverscrollBehaviorX, disp.mOverscrollBehaviorY); } ScrollStyles nsHTMLScrollFrame::GetScrollStyles() const { nsPresContext* presContext = PresContext(); if (!presContext->IsDynamic() && !(mIsRoot && presContext->HasPaginatedScrolling())) { return ScrollStyles(StyleOverflow::Hidden, StyleOverflow::Hidden); } if (!mIsRoot) { return ScrollStyles(*StyleDisplay(), ScrollStyles::MapOverflowToValidScrollStyle); } ScrollStyles result = presContext->GetViewportScrollStylesOverride(); if (nsDocShell* ds = presContext->GetDocShell()) { switch (ds->ScrollbarPreference()) { case ScrollbarPreference::Auto: break; case ScrollbarPreference::Never: result.mHorizontal = result.mVertical = StyleOverflow::Hidden; break; } } return result; } nsRect nsHTMLScrollFrame::GetLayoutScrollRange() const { return GetScrollRange(mScrollPort.width, mScrollPort.height); } nsRect nsHTMLScrollFrame::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 nsHTMLScrollFrame::GetVisualScrollRange() const { nsSize visualViewportSize = GetVisualViewportSize(); return GetScrollRange(visualViewportSize.width, visualViewportSize.height); } nsSize nsHTMLScrollFrame::GetVisualViewportSize() const { auto* presShell = PresShell(); if (mIsRoot && presShell->IsVisualViewportSizeSet()) { return presShell->GetVisualViewportSize(); } return mScrollPort.Size(); } nsPoint nsHTMLScrollFrame::GetVisualViewportOffset() const { if (mIsRoot) { auto* presShell = PresShell(); if (auto pendingUpdate = presShell->GetPendingVisualScrollUpdate()) { // The pending visual scroll update on the PresShell contains a raw, // unclamped offset (basically, whatever was passed to ScrollToVisual()). // It will be clamped on the APZ side, but if we use it as the // main-thread's visual viewport offset we need to clamp it ourselves. // Use GetScrollRangeForUserInputEvents() to do the clamping because this // the scroll range that APZ will use. return GetScrollRangeForUserInputEvents().ClampPoint( pendingUpdate->mVisualScrollOffset); } return presShell->GetVisualViewportOffset(); } return GetScrollPosition(); } bool nsHTMLScrollFrame::SetVisualViewportOffset(const nsPoint& aOffset, bool aRepaint) { MOZ_ASSERT(mIsRoot); AutoWeakFrame weakFrame(this); AutoScrollbarRepaintSuppression repaintSuppression(this, weakFrame, !aRepaint); bool retVal = PresShell()->SetVisualViewportOffset(aOffset, GetScrollPosition()); if (!weakFrame.IsAlive()) { return false; } return retVal; } nsRect nsHTMLScrollFrame::GetVisualOptimalViewingRect() const { auto* presShell = PresShell(); nsRect rect = mScrollPort; if (mIsRoot && presShell->IsVisualViewportSizeSet() && presShell->IsVisualViewportOffsetSet()) { rect = nsRect(mScrollPort.TopLeft() - GetScrollPosition() + presShell->GetVisualViewportOffset(), presShell->GetVisualViewportSize()); } // NOTE: We intentionally resolve scroll-padding percentages against the // scrollport even when the visual viewport is set, see // https://github.com/w3c/csswg-drafts/issues/4393. rect.Deflate(GetScrollPadding()); return rect; } static void AdjustDestinationForWholeDelta(const nsIntPoint& aDelta, const nsRect& aScrollRange, nsPoint& aPoint) { if (aDelta.x < 0) { aPoint.x = aScrollRange.X(); } else if (aDelta.x > 0) { aPoint.x = aScrollRange.XMost(); } if (aDelta.y < 0) { aPoint.y = aScrollRange.Y(); } else if (aDelta.y > 0) { aPoint.y = aScrollRange.YMost(); } } /** * 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 nsHTMLScrollFrame::ScrollBy(nsIntPoint aDelta, ScrollUnit aUnit, ScrollMode aMode, nsIntPoint* aOverflow, ScrollOrigin aOrigin, nsIScrollableFrame::ScrollMomentum aMomentum, ScrollSnapFlags aSnapFlags) { // 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 == ScrollOrigin::NotSpecified) { aOrigin = ScrollOrigin::Other; } bool isGenericOrigin = (aOrigin == ScrollOrigin::Other); bool askApzToDoTheScroll = false; if ((aSnapFlags == ScrollSnapFlags::Disabled || !NeedsScrollSnap()) && gfxPlatform::UseDesktopZoomingScrollbars() && nsLayoutUtils::AsyncPanZoomEnabled(this) && !nsLayoutUtils::ShouldDisableApzForElement(GetContent()) && (WantAsyncScroll() || mZoomableByAPZ) && CanApzScrollInTheseDirections(DirectionsInDelta(aDelta))) { askApzToDoTheScroll = true; } switch (aUnit) { case ScrollUnit::DEVICE_PIXELS: { nscoord appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); deltaMultiplier = nsSize(appUnitsPerDevPixel, appUnitsPerDevPixel); if (isGenericOrigin) { aOrigin = ScrollOrigin::Pixels; } negativeTolerance = positiveTolerance = 0.5f; break; } case ScrollUnit::LINES: { deltaMultiplier = GetLineScrollAmount(); if (isGenericOrigin) { aOrigin = ScrollOrigin::Lines; } negativeTolerance = positiveTolerance = 0.1f; break; } case ScrollUnit::PAGES: { deltaMultiplier = GetPageScrollAmount(); if (isGenericOrigin) { aOrigin = ScrollOrigin::Pages; } negativeTolerance = 0.05f; positiveTolerance = 0; break; } case ScrollUnit::WHOLE: { if (askApzToDoTheScroll) { MOZ_ASSERT(aDelta.x >= -1 && aDelta.x <= 1 && aDelta.y >= -1 && aDelta.y <= 1); deltaMultiplier = GetScrollRangeForUserInputEvents().Size(); break; } else { nsPoint pos = GetScrollPosition(); AdjustDestinationForWholeDelta(aDelta, GetLayoutScrollRange(), pos); ScrollToWithOrigin( pos, nullptr /* range */, ScrollOperationParams{aMode, ScrollOrigin::Other, aSnapFlags, ScrollTriggeredByScript::No}); // 'this' might be destroyed here if (aOverflow) { *aOverflow = nsIntPoint(0, 0); } return; } } default: NS_ERROR("Invalid scroll mode"); return; } if (askApzToDoTheScroll) { nsPoint delta( NSCoordSaturatingNonnegativeMultiply(aDelta.x, deltaMultiplier.width), NSCoordSaturatingNonnegativeMultiply(aDelta.y, deltaMultiplier.height)); AppendScrollUpdate( ScrollPositionUpdate::NewPureRelativeScroll(aOrigin, aMode, delta)); nsIContent* content = GetContent(); if (!DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(content)) { if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug)) { mozilla::layers::ScrollableLayerGuid::ViewID viewID = mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID; nsLayoutUtils::FindIDFor(content, &viewID); MOZ_LOG( sDisplayportLog, LogLevel::Debug, ("ScrollBy setting displayport on scrollId=%" PRIu64 "\n", viewID)); } DisplayPortUtils::CalculateAndSetDisplayPortMargins( GetScrollTargetFrame(), DisplayPortUtils::RepaintMode::Repaint); nsIFrame* frame = do_QueryFrame(GetScrollTargetFrame()); DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors( frame); } SchedulePaint(); return; } nsPoint newPos(NSCoordSaturatingAdd(mDestination.x, NSCoordSaturatingNonnegativeMultiply( aDelta.x, deltaMultiplier.width)), NSCoordSaturatingAdd(mDestination.y, NSCoordSaturatingNonnegativeMultiply( aDelta.y, deltaMultiplier.height))); Maybe snapDestination; if (aSnapFlags != ScrollSnapFlags::Disabled) { if (NeedsScrollSnap()) { nscoord appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); deltaMultiplier = nsSize(appUnitsPerDevPixel, appUnitsPerDevPixel); negativeTolerance = 0.1f; positiveTolerance = 0; ScrollUnit snapUnit = aUnit; if (aOrigin == ScrollOrigin::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 = ScrollUnit::LINES; } snapDestination = GetSnapPointForDestination(snapUnit, aSnapFlags, mDestination, newPos); if (snapDestination) { newPos = snapDestination->mPosition; } } } // 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); AutoWeakFrame weakFrame(this); ScrollToWithOrigin( newPos, &range, snapDestination ? ScrollOperationParams{aMode, aOrigin, std::move(snapDestination->mTargetIds)} : ScrollOperationParams{aMode, aOrigin}); if (!weakFrame.IsAlive()) { return; } if (aOverflow) { nsPoint clampAmount = newPos - mDestination; float appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); *aOverflow = nsIntPoint(NSAppUnitsToIntPixels(clampAmount.x, appUnitsPerDevPixel), NSAppUnitsToIntPixels(clampAmount.y, appUnitsPerDevPixel)); } if (aUnit == ScrollUnit::DEVICE_PIXELS && !nsLayoutUtils::AsyncPanZoomEnabled(this)) { // When APZ is disabled, we must track the velocity // on the main thread; otherwise, the APZC will manage this. mVelocityQueue.Sample(GetScrollPosition()); } } void nsHTMLScrollFrame::ScrollByCSSPixelsInternal(const CSSIntPoint& aDelta, ScrollMode aMode, ScrollSnapFlags aSnapFlags) { nsPoint current = GetScrollPosition(); // `current` value above might be a value which was aligned to in // layer-pixels, so starting from such points will make the difference between // the given position in script (e.g. scrollTo) and the aligned position // larger, in the worst case the difference can be observed in CSS pixels. // To avoid it, we use the current position in CSS pixels as the start // position. Hopefully it exactly matches the position where it was given by // the previous scrolling operation, but there may be some edge cases where // the current position in CSS pixels differs from the given position, the // cases should be fixed in bug 1556685. CSSPoint currentCSSPixels; if (StaticPrefs::layout_scroll_disable_pixel_alignment()) { currentCSSPixels = GetScrollPositionCSSPixels(); } else { currentCSSPixels = GetRoundedScrollPositionCSSPixels(); } nsPoint pt = CSSPoint::ToAppUnits(currentCSSPixels + aDelta); 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 (aDelta.x == 0.0f) { pt.x = current.x; range.x = pt.x; range.width = 0; } if (aDelta.y == 0.0f) { pt.y = current.y; range.y = pt.y; range.height = 0; } ScrollToWithOrigin( pt, &range, ScrollOperationParams{aMode, ScrollOrigin::Relative, aSnapFlags, ScrollTriggeredByScript::Yes}); // 'this' might be destroyed here } void nsHTMLScrollFrame::ScrollSnap(ScrollMode aMode) { float flingSensitivity = StaticPrefs::layout_css_scroll_snap_prediction_sensitivity(); int maxVelocity = StaticPrefs::layout_css_scroll_snap_prediction_max_velocity(); 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 nsHTMLScrollFrame::ScrollSnap(const nsPoint& aDestination, ScrollMode aMode) { nsRect scrollRange = GetLayoutScrollRange(); nsPoint pos = GetScrollPosition(); nsPoint destination = scrollRange.ClampPoint(aDestination); ScrollSnapFlags snapFlags = ScrollSnapFlags::IntendedEndPosition; if (mVelocityQueue.GetVelocity() != nsPoint()) { snapFlags |= ScrollSnapFlags::IntendedDirection; } // Bug 1776624 : Consider using mDestination as |aStartPos| argument for this // GetSnapPointForDestination call, this function call is the only one call // site using `GetScrollPosition()` as |aStartPos|. if (auto snapDestination = GetSnapPointForDestination( ScrollUnit::DEVICE_PIXELS, snapFlags, pos, destination)) { destination = snapDestination->mPosition; ScrollToWithOrigin( destination, nullptr /* range */, ScrollOperationParams{aMode, ScrollOrigin::Other, std::move(snapDestination->mTargetIds)}); } } nsSize nsHTMLScrollFrame::GetLineScrollAmount() const { RefPtr fm = nsLayoutUtils::GetInflatedFontMetricsForFrame(this); NS_ASSERTION(fm, "FontMetrics is null, assuming fontHeight == 1 appunit"); int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); nscoord minScrollAmountInAppUnits = std::max(1, StaticPrefs::mousewheel_min_line_scroll_amount()) * 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 and sticky-pos (that are * stuck) 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/sticky 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. * * If aViewportFrame is non-null then the scroll frame is the root scroll * frame and we should consider fixed-pos items. */ 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 void AddToListIfHeaderFooter(nsIFrame* aFrame, nsIFrame* aScrollPortFrame, const nsRect& aScrollPort, nsTArray& aList) { nsRect r = aFrame->GetRectRelativeToSelf(); r = nsLayoutUtils::TransformFrameRectToAncestor(aFrame, r, aScrollPortFrame); r = r.Intersect(aScrollPort); if ((r.width >= aScrollPort.width / 2 || r.width >= NSIntPixelsToAppUnits(800, AppUnitsPerCSSPixel())) && r.height <= aScrollPort.height / 3) { aList.AppendElement(TopAndBottom(r.y, r.YMost())); } } static nsSize GetScrollPortSizeExcludingHeadersAndFooters( nsIFrame* aScrollFrame, nsIFrame* aViewportFrame, const nsRect& aScrollPort) { AutoTArray list; if (aViewportFrame) { for (nsIFrame* f : aViewportFrame->GetChildList(FrameChildListID::Fixed)) { AddToListIfHeaderFooter(f, aViewportFrame, aScrollPort, list); } } // Add sticky frames that are currently in "fixed" positions StickyScrollContainer* ssc = StickyScrollContainer::GetStickyScrollContainerForScrollFrame( aScrollFrame); if (ssc) { const nsTArray& stickyFrames = ssc->GetFrames(); for (nsIFrame* f : stickyFrames) { // If it's acting like fixed position. if (ssc->IsStuckInYDirection(f)) { AddToListIfHeaderFooter(f, aScrollFrame, aScrollPort, list); } } } 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 nsHTMLScrollFrame::GetPageScrollAmount() const { nsSize effectiveScrollPortSize; if (GetVisualViewportSize() != mScrollPort.Size()) { // We want to use the visual viewport size if one is set. // The headers/footers adjustment is too complicated to do if there is a // visual viewport that differs from the layout viewport, this is probably // okay. effectiveScrollPortSize = GetVisualViewportSize(); } else { // Reduce effective scrollport height by the height of any // fixed-pos/sticky-pos headers or footers effectiveScrollPortSize = GetScrollPortSizeExcludingHeadersAndFooters( const_cast(this), mIsRoot ? PresShell()->GetRootFrame() : nullptr, mScrollPort); } nsSize lineScrollAmount = GetLineScrollAmount(); // 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 nsHTMLScrollFrame::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 // The layout offset we want to restore is the same as the visual offset // (for now, may change in bug 1499210), but clamped to the layout scroll // range (which can be a subset of the visual scroll range). // Note that we can't do the clamping when initializing mRestorePos in // RestoreState(), since the scrollable rect (which the clamping depends // on) can change over the course of the restoration process. nsPoint layoutRestorePos = GetLayoutScrollRange().ClampPoint(mRestorePos); nsPoint visualRestorePos = GetVisualScrollRange().ClampPoint(mRestorePos); // Continue restoring until both the layout and visual scroll positions // reach the destination. (Note that the two can only be different for // the root content document's root scroll frame, and when zoomed in). // This is necessary to avoid situations where the two offsets get stuck // at different values and nothing reconciles them (see bug 1519621 comment // 8). nsPoint logicalLayoutScrollPos = GetLogicalScrollPosition(); SCROLLRESTORE_LOG( "%p: ScrollToRestoredPosition (mRestorePos=%s, mLastPos=%s, " "layoutRestorePos=%s, visualRestorePos=%s, " "logicalLayoutScrollPos=%s, " "GetLogicalVisualViewportOffset()=%s)\n", this, ToString(mRestorePos).c_str(), ToString(mLastPos).c_str(), ToString(layoutRestorePos).c_str(), ToString(visualRestorePos).c_str(), ToString(logicalLayoutScrollPos).c_str(), ToString(GetLogicalVisualViewportOffset()).c_str()); // if we didn't move, we still need to restore if (GetLogicalVisualViewportOffset() == mLastPos || logicalLayoutScrollPos == 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 /* GetLogicalVisualViewportOffset() */ || layoutRestorePos != logicalLayoutScrollPos) { LoadingState state = GetPageLoadingState(); if (state == LoadingState::Stopped && !IsSubtreeDirty()) { return; } nsPoint visualScrollToPos = visualRestorePos; nsPoint layoutScrollToPos = layoutRestorePos; if (!IsPhysicalLTR()) { // convert from logical to physical scroll position visualScrollToPos.x -= (GetVisualViewportSize().width - mScrolledFrame->GetRect().width); layoutScrollToPos.x -= (GetVisualViewportSize().width - mScrolledFrame->GetRect().width); } AutoWeakFrame weakFrame(this); // It's very important to pass ScrollOrigin::Restore here, so // ScrollToWithOrigin won't clear out mRestorePos. ScrollToWithOrigin( layoutScrollToPos, nullptr, ScrollOperationParams{ScrollMode::Instant, ScrollOrigin::Restore}); if (!weakFrame.IsAlive()) { return; } if (mIsRoot) { PresShell()->ScrollToVisual(visualScrollToPos, FrameMetrics::eRestore, ScrollMode::Instant); } if (state == LoadingState::Loading || IsSubtreeDirty()) { // 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 = GetLogicalVisualViewportOffset(); 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; } } auto nsHTMLScrollFrame::GetPageLoadingState() -> LoadingState { bool loadCompleted = false, stopped = false; nsCOMPtr ds = GetContent()->GetComposedDoc()->GetDocShell(); if (ds) { nsCOMPtr viewer; ds->GetDocViewer(getter_AddRefs(viewer)); if (viewer) { loadCompleted = viewer->GetLoadCompleted(); stopped = viewer->GetIsStopped(); } } return loadCompleted ? (stopped ? LoadingState::Stopped : LoadingState::Loaded) : LoadingState::Loading; } nsHTMLScrollFrame::OverflowState nsHTMLScrollFrame::GetOverflowState() const { nsSize scrollportSize = mScrollPort.Size(); nsSize childSize = GetScrolledRect().Size(); OverflowState result = OverflowState::None; if (childSize.height > scrollportSize.height) { result |= OverflowState::Vertical; } if (childSize.width > scrollportSize.width) { result |= OverflowState::Horizontal; } return result; } nsresult nsHTMLScrollFrame::FireScrollPortEvent() { mAsyncScrollPortEvent.Forget(); // TODO(emilio): why do we need the whole WillPaintObserver infrastructure and // can't use AddScriptRunner & co? I guess it made sense when we used // WillPaintObserver for scroll events too, or when this used to flush. // // Should we remove this? OverflowState overflowState = GetOverflowState(); bool newVerticalOverflow = !!(overflowState & OverflowState::Vertical); bool vertChanged = mVerticalOverflow != newVerticalOverflow; bool newHorizontalOverflow = !!(overflowState & OverflowState::Horizontal); 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; RefPtr content = GetContent(); RefPtr presContext = PresContext(); return EventDispatcher::Dispatch(content, presContext, &event); } void nsHTMLScrollFrame::PostScrollEndEvent() { if (mScrollEndEvent) { return; } // The ScrollEndEvent constructor registers itself with the refresh driver. mScrollEndEvent = new ScrollEndEvent(this); } void nsHTMLScrollFrame::FireScrollEndEvent() { MOZ_ASSERT(GetContent()); MOZ_ASSERT(mScrollEndEvent); RefPtr presContext = PresContext(); mScrollEndEvent->Revoke(); mScrollEndEvent = nullptr; nsEventStatus status = nsEventStatus_eIgnore; WidgetGUIEvent event(true, eScrollend, nullptr); event.mFlags.mBubbles = mIsRoot; event.mFlags.mCancelable = false; RefPtr target = mIsRoot ? static_cast(presContext->Document()) : GetContent(); EventDispatcher::Dispatch(target, presContext, &event, nullptr, &status); } void nsHTMLScrollFrame::ReloadChildFrames() { mScrolledFrame = nullptr; mHScrollbarBox = nullptr; mVScrollbarBox = nullptr; mScrollCornerBox = nullptr; mResizerBox = nullptr; for (nsIFrame* frame : PrincipalChildList()) { nsIContent* content = frame->GetContent(); if (content == GetContent()) { NS_ASSERTION(!mScrolledFrame, "Already found the scrolled frame"); mScrolledFrame = frame; } else { nsAutoString value; if (content->IsElement()) { content->AsElement()->GetAttr(nsGkAtoms::orient, value); } if (!value.IsEmpty()) { // probably a scrollbar then if (value.LowerCaseEqualsLiteral("horizontal")) { NS_ASSERTION(!mHScrollbarBox, "Found multiple horizontal scrollbars?"); mHScrollbarBox = do_QueryFrame(frame); MOZ_ASSERT(mHScrollbarBox, "Not a scrollbar?"); } else { NS_ASSERTION(!mVScrollbarBox, "Found multiple vertical scrollbars?"); mVScrollbarBox = do_QueryFrame(frame); MOZ_ASSERT(mVScrollbarBox, "Not a scrollbar?"); } } 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; } } } } already_AddRefed nsHTMLScrollFrame::MakeScrollbar( NodeInfo* aNodeInfo, bool aVertical, AnonymousContentKey& aKey) { MOZ_ASSERT(aNodeInfo); MOZ_ASSERT( aNodeInfo->Equals(nsGkAtoms::scrollbar, nullptr, kNameSpaceID_XUL)); static constexpr nsLiteralString kOrientValues[2] = { u"horizontal"_ns, u"vertical"_ns, }; aKey = AnonymousContentKey::Type_Scrollbar; if (aVertical) { aKey |= AnonymousContentKey::Flag_Vertical; } RefPtr e; NS_TrustedNewXULElement(getter_AddRefs(e), do_AddRef(aNodeInfo)); #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. :( e->SetProperty(nsGkAtoms::restylableAnonymousNode, reinterpret_cast(true)); #endif // DEBUG e->SetAttr(kNameSpaceID_None, nsGkAtoms::orient, kOrientValues[aVertical], false); if (mIsRoot) { e->SetProperty(nsGkAtoms::docLevelNativeAnonymousContent, reinterpret_cast(true)); e->SetAttr(kNameSpaceID_None, nsGkAtoms::root_, u"true"_ns, false); // Don't bother making style caching take [root="true"] styles into account. aKey = AnonymousContentKey::None; } return e.forget(); } bool nsHTMLScrollFrame::IsForTextControlWithNoScrollbars() const { // FIXME(emilio): we should probably make the scroller inside an // internal pseudo-element, and then this would be simpler. // // Also, this could just use scrollbar-width these days. auto* content = GetContent(); if (!content) { return false; } auto* input = content->GetClosestNativeAnonymousSubtreeRootParentOrHost(); return input && input->IsHTMLElement(nsGkAtoms::input); } auto nsHTMLScrollFrame::GetCurrentAnonymousContent() const -> EnumSet { EnumSet result; if (mHScrollbarContent) { result += AnonymousContentType::HorizontalScrollbar; } if (mVScrollbarContent) { result += AnonymousContentType::VerticalScrollbar; } if (mResizerContent) { result += AnonymousContentType::Resizer; } return result; } auto nsHTMLScrollFrame::GetNeededAnonymousContent() const -> EnumSet { nsPresContext* pc = PresContext(); // 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 (pc->Document()->IsBeingUsedAsImage() || (!pc->IsDynamic() && !(mIsRoot && pc->HasPaginatedScrolling()))) { return {}; } if (IsForTextControlWithNoScrollbars()) { return {}; } EnumSet result; // 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!). // // TODO(emilio): Figure out if we can remove this special-case now that we // have more targeted optimizations. if (mIsRoot) { result += AnonymousContentType::HorizontalScrollbar; result += AnonymousContentType::VerticalScrollbar; // If scrollbar-width is none, don't generate scrollbars. } else if (StyleUIReset()->ScrollbarWidth() != StyleScrollbarWidth::None) { ScrollStyles styles = GetScrollStyles(); if (styles.mHorizontal != StyleOverflow::Hidden) { result += AnonymousContentType::HorizontalScrollbar; } if (styles.mVertical != StyleOverflow::Hidden) { result += AnonymousContentType::VerticalScrollbar; } } // 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 auto resizeStyle = StyleDisplay()->mResize; if (resizeStyle != StyleResize::None && !HasAnyStateBits(NS_FRAME_GENERATED_CONTENT)) { result += AnonymousContentType::Resizer; } return result; } nsresult nsHTMLScrollFrame::CreateAnonymousContent( nsTArray& aElements) { typedef nsIAnonymousContentCreator::ContentInfo ContentInfo; nsPresContext* presContext = PresContext(); nsNodeInfoManager* nodeInfoManager = presContext->Document()->NodeInfoManager(); auto neededAnonContent = GetNeededAnonymousContent(); if (neededAnonContent.isEmpty()) { return NS_OK; } { RefPtr nodeInfo = nodeInfoManager->GetNodeInfo( nsGkAtoms::scrollbar, nullptr, kNameSpaceID_XUL, nsINode::ELEMENT_NODE); NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY); if (neededAnonContent.contains(AnonymousContentType::HorizontalScrollbar)) { AnonymousContentKey key; mHScrollbarContent = MakeScrollbar(nodeInfo, /* aVertical */ false, key); aElements.AppendElement(ContentInfo(mHScrollbarContent, key)); } if (neededAnonContent.contains(AnonymousContentType::VerticalScrollbar)) { AnonymousContentKey key; mVScrollbarContent = MakeScrollbar(nodeInfo, /* aVertical */ true, key); aElements.AppendElement(ContentInfo(mVScrollbarContent, key)); } } if (neededAnonContent.contains(AnonymousContentType::Resizer)) { MOZ_ASSERT(!mIsRoot, "Root scroll frame shouldn't be resizable"); RefPtr nodeInfo; nodeInfo = nodeInfoManager->GetNodeInfo( nsGkAtoms::resizer, nullptr, kNameSpaceID_XUL, nsINode::ELEMENT_NODE); NS_ENSURE_TRUE(nodeInfo, NS_ERROR_OUT_OF_MEMORY); NS_TrustedNewXULElement(getter_AddRefs(mResizerContent), nodeInfo.forget()); nsAutoString dir; switch (StyleDisplay()->mResize) { case StyleResize::Horizontal: if (IsScrollbarOnRight()) { dir.AssignLiteral("right"); } else { dir.AssignLiteral("left"); } break; case StyleResize::Vertical: dir.AssignLiteral("bottom"); if (!IsScrollbarOnRight()) { mResizerContent->SetAttr(kNameSpaceID_None, nsGkAtoms::flip, u""_ns, false); } break; case StyleResize::Both: if (IsScrollbarOnRight()) { dir.AssignLiteral("bottomright"); } else { dir.AssignLiteral("bottomleft"); } break; default: NS_WARNING("only resizable types should have resizers"); } mResizerContent->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, dir, false); aElements.AppendElement(mResizerContent); } if (neededAnonContent.contains(AnonymousContentType::HorizontalScrollbar) && neededAnonContent.contains(AnonymousContentType::VerticalScrollbar)) { AnonymousContentKey key = AnonymousContentKey::Type_ScrollCorner; RefPtr nodeInfo = nodeInfoManager->GetNodeInfo(nsGkAtoms::scrollcorner, nullptr, kNameSpaceID_XUL, nsINode::ELEMENT_NODE); NS_TrustedNewXULElement(getter_AddRefs(mScrollCornerContent), nodeInfo.forget()); if (mIsRoot) { mScrollCornerContent->SetProperty( nsGkAtoms::docLevelNativeAnonymousContent, reinterpret_cast(true)); mScrollCornerContent->SetAttr(kNameSpaceID_None, nsGkAtoms::root_, u"true"_ns, false); // Don't bother making style caching take [root="true"] styles into // account. key = AnonymousContentKey::None; } aElements.AppendElement(ContentInfo(mScrollCornerContent, key)); } // Don't cache styles if we are a child of a 's attributes. if (GetContent()->IsHTMLElement(nsGkAtoms::select)) { for (auto& info : aElements) { info.mKey = AnonymousContentKey::None; } } return NS_OK; } void nsHTMLScrollFrame::AppendAnonymousContentTo( nsTArray& 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 nsHTMLScrollFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { nsContainerFrame::DidSetComputedStyle(aOldComputedStyle); if (aOldComputedStyle && !mIsRoot && StyleDisplay()->mScrollSnapType != aOldComputedStyle->StyleDisplay()->mScrollSnapType) { PostPendingResnap(); } } void nsHTMLScrollFrame::RemoveObservers() { if (mAsyncScroll) { mAsyncScroll->RemoveObserver(); mAsyncScroll = nullptr; } if (mAsyncSmoothMSDScroll) { mAsyncSmoothMSDScroll->RemoveObserver(); mAsyncSmoothMSDScroll = 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 nsHTMLScrollFrame::UpdateScrollbarPosition() { AutoWeakFrame weakFrame(this); mFrameIsUpdatingScrollbar = true; nsPoint pt = GetScrollPosition(); nsRect scrollRange = GetVisualScrollRange(); if (gfxPlatform::UseDesktopZoomingScrollbars()) { pt = GetVisualViewportOffset(); scrollRange = GetScrollRangeForUserInputEvents(); } if (mVScrollbarBox) { SetCoordAttribute(mVScrollbarBox->GetContent()->AsElement(), nsGkAtoms::curpos, pt.y - scrollRange.y); if (!weakFrame.IsAlive()) { return; } } if (mHScrollbarBox) { SetCoordAttribute(mHScrollbarBox->GetContent()->AsElement(), nsGkAtoms::curpos, pt.x - scrollRange.x); if (!weakFrame.IsAlive()) { return; } } mFrameIsUpdatingScrollbar = false; } void nsHTMLScrollFrame::CurPosAttributeChangedInternal(nsIContent* aContent, bool aDoScroll) { NS_ASSERTION(aContent, "aContent must not be null"); NS_ASSERTION((mHScrollbarBox && mHScrollbarBox->GetContent() == aContent) || (mVScrollbarBox && mVScrollbarBox->GetContent() == aContent), "unexpected child"); MOZ_ASSERT(aContent->IsElement()); // 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 scrollRange = GetVisualScrollRange(); nsPoint current = GetScrollPosition() - scrollRange.TopLeft(); if (gfxPlatform::UseDesktopZoomingScrollbars()) { scrollRange = GetScrollRangeForUserInputEvents(); current = GetVisualViewportOffset() - scrollRange.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 += scrollRange.TopLeft(); dest += scrollRange.TopLeft(); allowedRange += scrollRange.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 && (mHasHorizontalScrollbar || mHasVerticalScrollbar)) { RefPtr scrollbarActivity(mScrollbarActivity); scrollbarActivity->ActivityOccurred(); } const bool isSmooth = aContent->AsElement()->HasAttr(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. AutoWeakFrame weakFrame(this); UpdateScrollbarPosition(); if (!weakFrame.IsAlive()) { return; } } if (aDoScroll) { ScrollToWithOrigin(dest, &allowedRange, ScrollOperationParams{ isSmooth ? ScrollMode::Smooth : ScrollMode::Instant, ScrollOrigin::Scrollbars}); } // 'this' might be destroyed here } /* ============= Scroll events ========== */ nsHTMLScrollFrame::ScrollEvent::ScrollEvent(nsHTMLScrollFrame* aHelper, bool aDelayed) : Runnable("nsHTMLScrollFrame::ScrollEvent"), mHelper(aHelper) { mHelper->PresContext()->RefreshDriver()->PostScrollEvent(this, aDelayed); } // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsHTMLScrollFrame::ScrollEvent::Run() { if (mHelper) { mHelper->FireScrollEvent(); } return NS_OK; } nsHTMLScrollFrame::ScrollEndEvent::ScrollEndEvent(nsHTMLScrollFrame* aHelper) : Runnable("nsHTMLScrollFrame::ScrollEndEvent"), mHelper(aHelper) { mHelper->PresContext()->RefreshDriver()->PostScrollEvent(this); } MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsHTMLScrollFrame::ScrollEndEvent::Run() { if (mHelper) { mHelper->FireScrollEndEvent(); } return NS_OK; } void nsHTMLScrollFrame::FireScrollEvent() { RefPtr content = GetContent(); RefPtr presContext = PresContext(); AUTO_PROFILER_TRACING_MARKER_DOCSHELL("Paint", "FireScrollEvent", GRAPHICS, presContext->GetDocShell()); MOZ_ASSERT(mScrollEvent); mScrollEvent->Revoke(); mScrollEvent = nullptr; // If event handling is suppressed, keep posting the scroll event to the // refresh driver until it is unsuppressed. The event is marked as delayed so // that the refresh driver does not continue ticking. if (content->GetComposedDoc() && content->GetComposedDoc()->EventHandlingSuppressed()) { content->GetComposedDoc()->SetHasDelayedRefreshEvent(); PostScrollEvent(/* aDelayed = */ true); return; } bool oldProcessing = mProcessingScrollEvent; AutoWeakFrame weakFrame(this); auto RestoreProcessingScrollEvent = mozilla::MakeScopeExit([&] { if (weakFrame.IsAlive()) { // Otherwise `this` will be dead too. mProcessingScrollEvent = oldProcessing; } }); mProcessingScrollEvent = true; WidgetGUIEvent event(true, eScroll, nullptr); nsEventStatus status = nsEventStatus_eIgnore; // Fire viewport scroll events at the document (where they // will bubble to the window) mozilla::layers::ScrollLinkedEffectDetector detector( content->GetComposedDoc(), presContext->RefreshDriver()->MostRecentRefresh()); if (mIsRoot) { if (RefPtr doc = content->GetUncomposedDoc()) { 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); } } void nsHTMLScrollFrame::PostScrollEvent(bool aDelayed) { if (mScrollEvent) { return; } // The ScrollEvent constructor registers itself with the refresh driver. mScrollEvent = new ScrollEvent(this, aDelayed); } // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsHTMLScrollFrame::AsyncScrollPortEvent::Run() { return mHelper ? mHelper->FireScrollPortEvent() : NS_OK; } void nsHTMLScrollFrame::PostOverflowEvent() { if (mAsyncScrollPortEvent.IsPending()) { return; } OverflowState overflowState = GetOverflowState(); bool newVerticalOverflow = !!(overflowState & OverflowState::Vertical); bool vertChanged = mVerticalOverflow != newVerticalOverflow; bool newHorizontalOverflow = !!(overflowState & OverflowState::Horizontal); bool horizChanged = mHorizontalOverflow != newHorizontalOverflow; if (!vertChanged && !horizChanged) { return; } nsRootPresContext* rpc = PresContext()->GetRootPresContext(); if (!rpc) { return; } mAsyncScrollPortEvent = new AsyncScrollPortEvent(this); rpc->AddWillPaintObserver(mAsyncScrollPortEvent.get()); } nsIFrame* nsHTMLScrollFrame::GetFrameForStyle() const { nsIFrame* styleFrame = nullptr; if (mIsRoot) { if (const Element* rootElement = PresContext()->Document()->GetRootElement()) { styleFrame = rootElement->GetPrimaryFrame(); } } else { styleFrame = const_cast(this); } return styleFrame; } bool nsHTMLScrollFrame::NeedsScrollSnap() const { nsIFrame* scrollSnapFrame = GetFrameForStyle(); if (!scrollSnapFrame) { return false; } return scrollSnapFrame->StyleDisplay()->mScrollSnapType.strictness != StyleScrollSnapStrictness::None; } nsSize nsHTMLScrollFrame::GetSnapportSize() const { nsRect snapport = GetScrollPortRect(); nsMargin scrollPadding = GetScrollPadding(); snapport.Deflate(scrollPadding); return snapport.Size(); } bool nsHTMLScrollFrame::IsScrollbarOnRight() const { // 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 (StaticPrefs::layout_scrollbar_side()) { default: case 0: // UI directionality return StaticPrefs::bidi_direction() == IBMBIDI_TEXTDIRECTION_LTR; case 1: // Document / content directionality return IsPhysicalLTR(); case 2: // Always right return true; case 3: // Always left return false; } } bool nsHTMLScrollFrame::IsScrollingActive() const { const nsStyleDisplay* disp = StyleDisplay(); if (disp->mWillChange.bits & StyleWillChangeBits::SCROLL) { return true; } nsIContent* content = GetContent(); return mHasBeenScrolledRecently || IsAlwaysActive() || DisplayPortUtils::HasDisplayPort(content) || nsContentUtils::HasScrollgrab(content); } void nsHTMLScrollFrame::FinishReflowForScrollbar(Element* aElement, nscoord aMinXY, nscoord aMaxXY, nscoord aCurPosXY, nscoord aPageIncrement, nscoord aIncrement) { // Scrollbars assume zero is the minimum position, so translate for them. SetCoordAttribute(aElement, nsGkAtoms::curpos, aCurPosXY - aMinXY); SetScrollbarEnabled(aElement, aMaxXY - aMinXY); SetCoordAttribute(aElement, nsGkAtoms::maxpos, aMaxXY - aMinXY); SetCoordAttribute(aElement, nsGkAtoms::pageincrement, aPageIncrement); SetCoordAttribute(aElement, nsGkAtoms::increment, aIncrement); } class MOZ_RAII AutoMinimumScaleSizeChangeDetector final { public: explicit AutoMinimumScaleSizeChangeDetector( nsHTMLScrollFrame* ansHTMLScrollFrame) : mHelper(ansHTMLScrollFrame) { MOZ_ASSERT(mHelper); MOZ_ASSERT(mHelper->mIsRoot); mPreviousMinimumScaleSize = ansHTMLScrollFrame->mMinimumScaleSize; mPreviousIsUsingMinimumScaleSize = ansHTMLScrollFrame->mIsUsingMinimumScaleSize; } ~AutoMinimumScaleSizeChangeDetector() { if (mPreviousMinimumScaleSize != mHelper->mMinimumScaleSize || mPreviousIsUsingMinimumScaleSize != mHelper->mIsUsingMinimumScaleSize) { mHelper->mMinimumScaleSizeChanged = true; } } private: nsHTMLScrollFrame* mHelper; nsSize mPreviousMinimumScaleSize; bool mPreviousIsUsingMinimumScaleSize; }; nsSize nsHTMLScrollFrame::TrueOuterSize(nsDisplayListBuilder* aBuilder) const { if (!PresShell()->UsesMobileViewportSizing()) { return GetSize(); } RefPtr manager = PresShell()->GetMobileViewportManager(); MOZ_ASSERT(manager); LayoutDeviceIntSize displaySize = manager->DisplaySize(); MOZ_ASSERT(aBuilder); // In case of WebRender, we expand the outer size to include the dynamic // toolbar area here. // In case of non WebRender, we expand the size dynamically in // MoveScrollbarForLayerMargin in AsyncCompositionManager.cpp. WebRenderLayerManager* layerManager = aBuilder->GetWidgetLayerManager(); if (layerManager) { displaySize.height += ViewAs( PresContext()->GetDynamicToolbarMaxHeight(), PixelCastJustification::LayoutDeviceIsScreenForBounds); } return LayoutDeviceSize::ToAppUnits(displaySize, PresContext()->AppUnitsPerDevPixel()); } void nsHTMLScrollFrame::UpdateMinimumScaleSize( const nsRect& aScrollableOverflow, const nsSize& aICBSize) { MOZ_ASSERT(mIsRoot); AutoMinimumScaleSizeChangeDetector minimumScaleSizeChangeDetector(this); mIsUsingMinimumScaleSize = false; if (!PresShell()->UsesMobileViewportSizing()) { return; } nsPresContext* pc = PresContext(); MOZ_ASSERT(pc->IsRootContentDocumentCrossProcess(), "The pres context should be for the root content document"); RefPtr manager = PresShell()->GetMobileViewportManager(); MOZ_ASSERT(manager); ScreenIntSize displaySize = ViewAs( manager->DisplaySize(), PixelCastJustification::LayoutDeviceIsScreenForBounds); if (displaySize.width == 0 || displaySize.height == 0) { return; } if (aScrollableOverflow.IsEmpty()) { // Bail if the scrollable overflow rect is empty, as we're going to be // dividing by it. return; } Document* doc = pc->Document(); MOZ_ASSERT(doc, "The document should be valid"); if (doc->GetFullscreenElement()) { // Don't use the minimum scale size in the case of fullscreen state. // FIXME: 1508177: We will no longer need this. return; } nsViewportInfo viewportInfo = doc->GetViewportInfo(displaySize); if (!viewportInfo.IsZoomAllowed()) { // Don't apply the minimum scale size if user-scalable=no is specified. return; } // The intrinsic minimum scale is the scale that fits the entire content // width into the visual viewport. CSSToScreenScale intrinsicMinScale( displaySize.width / CSSRect::FromAppUnits(aScrollableOverflow).XMost()); // The scale used to compute the minimum-scale size is the larger of the // intrinsic minimum and the min-scale from the meta viewport tag. CSSToScreenScale minScale = std::max(intrinsicMinScale, viewportInfo.GetMinZoom()); // The minimum-scale size is the size of the visual viewport when zoomed // to be the minimum scale. mMinimumScaleSize = CSSSize::ToAppUnits(ScreenSize(displaySize) / minScale); // Ensure the minimum-scale size is never smaller than the ICB size. // That could happen if a page has a meta viewport tag with large explicitly // specified viewport dimensions (making the ICB large) and also a large // minimum scale (making the min-scale size small). mMinimumScaleSize = Max(aICBSize, mMinimumScaleSize); mIsUsingMinimumScaleSize = true; } bool nsHTMLScrollFrame::ReflowFinished() { mPostedReflowCallback = false; TryScheduleScrollAnimations(); if (mIsRoot) { if (mMinimumScaleSizeChanged && PresShell()->UsesMobileViewportSizing() && !PresShell()->IsResolutionUpdatedByApz()) { RefPtr manager = PresShell()->GetMobileViewportManager(); MOZ_ASSERT(manager); manager->ShrinkToDisplaySizeIfNeeded(); mMinimumScaleSizeChanged = false; } if (!UsesOverlayScrollbars()) { // Layout scrollbars may have added or removed during reflow, so let's // update the visual viewport accordingly. Note that this may be a no-op // because we might have recomputed the visual viewport size during the // reflow itself, just before laying out the fixed-pos items. But there // might be cases where that code doesn't run, so this is a sort of // backstop to ensure we do that recomputation. if (RefPtr manager = PresShell()->GetMobileViewportManager()) { manager->UpdateVisualViewportSizeForPotentialScrollbarChange(); } } #if defined(MOZ_WIDGET_ANDROID) const bool hasVerticalOverflow = GetOverflowState() & OverflowState::Vertical && GetScrollStyles().mVertical != StyleOverflow::Hidden; if (!mFirstReflow && mHasVerticalOverflowForDynamicToolbar && !hasVerticalOverflow) { PresShell()->MaybeNotifyShowDynamicToolbar(); } mHasVerticalOverflowForDynamicToolbar = hasVerticalOverflow; #endif // defined(MOZ_WIDGET_ANDROID) } bool doScroll = true; if (IsSubtreeDirty()) { // We will get another call after the next reflow and scrolling // later is less janky. doScroll = false; } if (mFirstReflow) { nsPoint currentScrollPos = GetScrollPosition(); if (!mScrollUpdates.IsEmpty() && mScrollUpdates.LastElement().GetOrigin() == ScrollOrigin::None && currentScrollPos != nsPoint()) { // With frame reconstructions, the reconstructed frame may have a nonzero // scroll position by the end of the reflow, but without going through // RestoreState. In particular this can happen with RTL XUL scrollframes, // see https://bugzilla.mozilla.org/show_bug.cgi?id=1664638#c14. // Upon construction, the nsHTMLScrollFrame constructor will have inserted // a ScrollPositionUpdate into mScrollUpdates with origin None and a zero // scroll position, but here we update that to hold the correct scroll // position. Otherwise APZ may end up resetting the scroll position to // zero incorrectly. If we ever hit this codepath, it must be on a reflow // immediately following the scrollframe construction, so there should be // exactly one ScrollPositionUpdate in mScrollUpdates. MOZ_ASSERT(mScrollUpdates.Length() == 1); MOZ_ASSERT(mScrollUpdates.LastElement().GetGeneration() == mScrollGeneration); MOZ_ASSERT(mScrollUpdates.LastElement().GetDestination() == CSSPoint()); SCROLLRESTORE_LOG("%p: updating initial SPU to pos %s\n", this, ToString(currentScrollPos).c_str()); mScrollUpdates.Clear(); AppendScrollUpdate( ScrollPositionUpdate::NewScrollframe(currentScrollPos)); } mFirstReflow = false; } nsAutoScriptBlocker scriptBlocker; if (mReclampVVOffsetInReflowFinished) { MOZ_ASSERT(mIsRoot && PresShell()->IsVisualViewportOffsetSet()); mReclampVVOffsetInReflowFinished = false; AutoWeakFrame weakFrame(this); PresShell()->SetVisualViewportOffset(PresShell()->GetVisualViewportOffset(), GetScrollPosition()); NS_ENSURE_TRUE(weakFrame.IsAlive(), false); } if (doScroll) { ScrollToRestoredPosition(); // Clamp current scroll position to new bounds. Normally this won't // do anything. nsPoint currentScrollPos = GetScrollPosition(); ScrollToImpl(currentScrollPos, nsRect(currentScrollPos, nsSize(0, 0)), ScrollOrigin::Clamp); if (ScrollAnimationState().isEmpty()) { // 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. if (mMayHaveDirtyFixedChildren) { mMayHaveDirtyFixedChildren = false; nsIFrame* parentFrame = GetParent(); for (nsIFrame* fixedChild = parentFrame->GetChildList(FrameChildListID::Fixed).FirstChild(); fixedChild; fixedChild = fixedChild->GetNextSibling()) { // force a reflow of the fixed child PresShell()->FrameNeedsReflow(fixedChild, IntrinsicDirty::None, NS_FRAME_HAS_DIRTY_CHILDREN); } } // Suppress handling of the curpos attribute changes we make here. NS_ASSERTION(!mFrameIsUpdatingScrollbar, "We shouldn't be reentering here"); mFrameIsUpdatingScrollbar = true; // FIXME(emilio): Why this instead of mHScrollbarContent / mVScrollbarContent? RefPtr vScroll = mVScrollbarBox ? mVScrollbarBox->GetContent()->AsElement() : nullptr; RefPtr hScroll = mHScrollbarBox ? mHScrollbarBox->GetContent()->AsElement() : nullptr; // Note, in some cases this 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) { nsSize visualViewportSize = GetVisualViewportSize(); nsRect scrollRange = GetVisualScrollRange(); nsPoint scrollPos = GetScrollPosition(); nsSize lineScrollAmount = GetLineScrollAmount(); if (gfxPlatform::UseDesktopZoomingScrollbars()) { scrollRange = GetScrollRangeForUserInputEvents(); scrollPos = GetVisualViewportOffset(); } // If modifying the logic here, be sure to modify the corresponding // compositor-side calculation in ScrollThumbUtils::ApplyTransformForAxis(). AutoWeakFrame weakFrame(this); if (vScroll) { const double kScrollMultiplier = StaticPrefs::toolkit_scrollbox_verticalScrollDistance(); nscoord increment = lineScrollAmount.height * kScrollMultiplier; // We normally use (visualViewportSize.height - increment) for height of // page scrolling. However, it is too small when increment is very large. // (If increment is larger than visualViewportSize.height, direction of // scrolling will be opposite). To avoid it, we use // (float(visualViewportSize.height) * 0.8) as lower bound value of height // of page scrolling. (bug 383267) // XXX shouldn't we use GetPageScrollAmount here? nscoord pageincrement = nscoord(visualViewportSize.height - increment); nscoord pageincrementMin = nscoord(float(visualViewportSize.height) * 0.8); FinishReflowForScrollbar( vScroll, scrollRange.y, scrollRange.YMost(), scrollPos.y, std::max(pageincrement, pageincrementMin), increment); } if (hScroll) { const double kScrollMultiplier = StaticPrefs::toolkit_scrollbox_horizontalScrollDistance(); nscoord increment = lineScrollAmount.width * kScrollMultiplier; FinishReflowForScrollbar( hScroll, scrollRange.x, scrollRange.XMost(), scrollPos.x, nscoord(float(visualViewportSize.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; } CurPosAttributeChangedInternal( mVScrollbarBox ? mVScrollbarBox->GetContent()->AsElement() : mHScrollbarBox->GetContent()->AsElement(), doScroll); return doScroll; } void nsHTMLScrollFrame::ReflowCallbackCanceled() { mPostedReflowCallback = false; } bool nsHTMLScrollFrame::ComputeCustomOverflow(OverflowAreas& aOverflowAreas) { ScrollStyles ss = GetScrollStyles(); // 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(); ScrollDirections overflowChange = GetOverflowChange(scrolledRect, mPrevScrolledRect); mPrevScrolledRect = scrolledRect; bool needReflow = false; nsPoint scrollPosition = GetScrollPosition(); if (overflowChange.contains(ScrollDirection::eHorizontal)) { if (ss.mHorizontal != StyleOverflow::Hidden || scrollPosition.x) { needReflow = true; } } if (overflowChange.contains(ScrollDirection::eVertical)) { if (ss.mVertical != StyleOverflow::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. PresShell()->FrameNeedsReflow(this, IntrinsicDirty::None, 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 nsContainerFrame::ComputeCustomOverflow(aOverflowAreas); } void nsHTMLScrollFrame::UpdateSticky() { StickyScrollContainer* ssc = StickyScrollContainer::GetStickyScrollContainerForScrollFrame(this); if (ssc) { ssc->UpdatePositions(GetScrollPosition(), this); } } void nsHTMLScrollFrame::UpdatePrevScrolledRect() { // The layout scroll range is determinated by the scrolled rect and the scroll // port, so if the scrolled rect is updated, we may have to schedule the // associated scroll-driven animations' restyles. nsRect currScrolledRect = GetScrolledRect(); if (!currScrolledRect.IsEqualEdges(mPrevScrolledRect)) { mMayScheduleScrollAnimations = true; } mPrevScrolledRect = currScrolledRect; } void nsHTMLScrollFrame::AdjustScrollbarRectForResizer( nsIFrame* aFrame, nsPresContext* aPresContext, nsRect& aRect, bool aHasResizer, ScrollDirection aDirection) { if ((aDirection == ScrollDirection::eVertical ? 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))) { switch (aDirection) { case ScrollDirection::eVertical: aRect.height = std::max(0, resizerRect.y - aRect.y); break; case ScrollDirection::eHorizontal: aRect.width = std::max(0, resizerRect.x - aRect.x); break; } } else if (resizerRect.Contains(aRect.BottomLeft() + nsPoint(1, -1))) { switch (aDirection) { case ScrollDirection::eVertical: aRect.height = std::max(0, resizerRect.y - aRect.y); break; case ScrollDirection::eHorizontal: { nscoord xmost = aRect.XMost(); aRect.x = std::max(aRect.x, resizerRect.XMost()); aRect.width = xmost - aRect.x; break; } } } } 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 nsHTMLScrollFrame::LayoutScrollbarPartAtRect( const ScrollReflowInput& aState, ReflowInput& aKidReflowInput, const nsRect& aRect) { nsPresContext* pc = PresContext(); nsIFrame* kid = aKidReflowInput.mFrame; const auto wm = kid->GetWritingMode(); ReflowOutput desiredSize(wm); MOZ_ASSERT(!wm.IsVertical(), "Scrollbar parts should have writing-mode: initial"); MOZ_ASSERT(!wm.IsInlineReversed(), "Scrollbar parts should have writing-mode: initial"); // XXX Maybe get a meaningful container size or something. Shouldn't matter // given our asserts above. const nsSize containerSize; aKidReflowInput.SetComputedISize(aRect.Width()); aKidReflowInput.SetComputedBSize(aRect.Height()); const LogicalPoint pos(wm, aRect.TopLeft(), containerSize); const auto flags = ReflowChildFlags::Default; nsReflowStatus status; ReflowOutput kidDesiredSize(wm); ReflowChild(kid, pc, kidDesiredSize, aKidReflowInput, wm, pos, containerSize, flags, status); FinishReflowChild(kid, pc, kidDesiredSize, &aKidReflowInput, wm, pos, containerSize, flags); } void nsHTMLScrollFrame::LayoutScrollbars(ScrollReflowInput& aState, const nsRect& aInsideBorderArea, const nsRect& aOldScrollPort) { NS_ASSERTION(!mSuppressScrollbarUpdate, "This should have been suppressed"); const bool scrollbarOnLeft = !IsScrollbarOnRight(); const bool overlayScrollbars = UsesOverlayScrollbars(); const bool overlayScrollBarsOnRoot = overlayScrollbars && mIsRoot; const bool showVScrollbar = mVScrollbarBox && mHasVerticalScrollbar; const bool showHScrollbar = mHScrollbarBox && mHasHorizontalScrollbar; nsSize compositionSize = mScrollPort.Size(); if (overlayScrollBarsOnRoot) { compositionSize = nsLayoutUtils::CalculateCompositionSizeForFrame( this, false, &compositionSize); } nsPresContext* presContext = mScrolledFrame->PresContext(); nsRect vRect; if (showVScrollbar) { vRect.height = overlayScrollBarsOnRoot ? compositionSize.height : mScrollPort.height; vRect.y = mScrollPort.y; if (scrollbarOnLeft) { vRect.width = mScrollPort.x - aInsideBorderArea.x; vRect.x = aInsideBorderArea.x; } else { vRect.width = aInsideBorderArea.XMost() - mScrollPort.XMost(); vRect.x = mScrollPort.x + compositionSize.width; } if (overlayScrollbars || mOnlyNeedVScrollbarToScrollVVInsideLV) { const nscoord width = aState.VScrollbarPrefWidth(); // There is no space reserved for the layout scrollbar, it is currently // not visible because it is positioned just outside the scrollport. But // we know that it needs to be made visible so we shift it back in. vRect.width += width; if (!scrollbarOnLeft) { vRect.x -= width; } } } nsRect hRect; if (showHScrollbar) { hRect.width = overlayScrollBarsOnRoot ? compositionSize.width : mScrollPort.width; hRect.x = mScrollPort.x; hRect.height = aInsideBorderArea.YMost() - mScrollPort.YMost(); hRect.y = mScrollPort.y + compositionSize.height; if (overlayScrollbars || mOnlyNeedHScrollbarToScrollVVInsideLV) { const nscoord height = aState.HScrollbarPrefHeight(); hRect.height += height; // There is no space reserved for the layout scrollbar, it is currently // not visible because it is positioned just outside the scrollport. But // we know that it needs to be made visible so we shift it back in. hRect.y -= height; } } const bool hasVisualOnlyScrollbarsOnBothDirections = !overlayScrollbars && showHScrollbar && mOnlyNeedHScrollbarToScrollVVInsideLV && showVScrollbar && mOnlyNeedVScrollbarToScrollVVInsideLV; nsPresContext* pc = PresContext(); // place the scrollcorner if (mScrollCornerBox) { nsRect r(0, 0, 0, 0); if (scrollbarOnLeft) { // scrollbar (if any) on left r.width = showVScrollbar ? mScrollPort.x - aInsideBorderArea.x : 0; r.x = aInsideBorderArea.x; } else { // scrollbar (if any) on right r.width = showVScrollbar ? aInsideBorderArea.XMost() - mScrollPort.XMost() : 0; r.x = aInsideBorderArea.XMost() - r.width; } NS_ASSERTION(r.width >= 0, "Scroll area should be inside client rect"); if (showHScrollbar) { // scrollbar (if any) on bottom // Note we don't support the horizontal scrollbar at the top side. r.height = aInsideBorderArea.YMost() - mScrollPort.YMost(); NS_ASSERTION(r.height >= 0, "Scroll area should be inside client rect"); } r.y = aInsideBorderArea.YMost() - r.height; // If we have layout scrollbars and both scrollbars are present and both are // only needed to scroll the VV inside the LV then we need a scrollcorner // but the above calculation will result in an empty rect, so adjust it. if (r.IsEmpty() && hasVisualOnlyScrollbarsOnBothDirections) { r.width = vRect.width; r.height = hRect.height; r.x = scrollbarOnLeft ? mScrollPort.x : mScrollPort.XMost() - r.width; r.y = mScrollPort.YMost() - r.height; } ReflowInput scrollCornerRI( pc, aState.mReflowInput, mScrollCornerBox, LogicalSize(mScrollCornerBox->GetWritingMode(), r.Size())); LayoutScrollbarPartAtRect(aState, scrollCornerRI, r); } if (mResizerBox) { // If a resizer is present, get its size. // // TODO(emilio): Should this really account for scrollbar-width? auto scrollbarWidth = nsLayoutUtils::StyleForScrollbar(this) ->StyleUIReset() ->ScrollbarWidth(); const nscoord scrollbarSize = GetNonOverlayScrollbarSize(pc, scrollbarWidth); ReflowInput resizerRI(pc, aState.mReflowInput, mResizerBox, LogicalSize(mResizerBox->GetWritingMode())); nsSize resizerMinSize = {resizerRI.ComputedMinWidth(), resizerRI.ComputedMinHeight()}; nsRect r; r.width = std::max(std::max(r.width, scrollbarSize), resizerMinSize.width); r.x = scrollbarOnLeft ? aInsideBorderArea.x : aInsideBorderArea.XMost() - r.width; r.height = std::max(std::max(r.height, scrollbarSize), resizerMinSize.height); r.y = aInsideBorderArea.YMost() - r.height; LayoutScrollbarPartAtRect(aState, resizerRI, r); } // Note that AdjustScrollbarRectForResizer has to be called after the // resizer has been laid out immediately above this because it gets the rect // of the resizer frame. if (mVScrollbarBox) { AdjustScrollbarRectForResizer(this, presContext, vRect, mResizerBox, ScrollDirection::eVertical); } if (mHScrollbarBox) { AdjustScrollbarRectForResizer(this, presContext, hRect, mResizerBox, ScrollDirection::eHorizontal); } // Layout scrollbars can overlap at this point if they are both present and // both only needed to scroll the VV inside the LV. if (!LookAndFeel::GetInt(LookAndFeel::IntID::AllowOverlayScrollbarsOverlap) || hasVisualOnlyScrollbarsOnBothDirections) { AdjustOverlappingScrollbars(vRect, hRect); } if (mVScrollbarBox) { ReflowInput vScrollbarRI( pc, aState.mReflowInput, mVScrollbarBox, LogicalSize(mVScrollbarBox->GetWritingMode(), vRect.Size())); LayoutScrollbarPartAtRect(aState, vScrollbarRI, vRect); } if (mHScrollbarBox) { ReflowInput hScrollbarRI( pc, aState.mReflowInput, mHScrollbarBox, LogicalSize(mHScrollbarBox->GetWritingMode(), hRect.Size())); LayoutScrollbarPartAtRect(aState, hScrollbarRI, 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 (aOldScrollPort.Size() != mScrollPort.Size() && !HasAnyStateBits(NS_FRAME_IS_DIRTY) && mIsRoot) { mMayHaveDirtyFixedChildren = true; } // post reflow callback to modify scrollbar attributes mUpdateScrollbarAttributes = true; if (!mPostedReflowCallback) { PresShell()->PostReflowCallback(this); mPostedReflowCallback = true; } } #if DEBUG static bool ShellIsAlive(nsWeakPtr& aWeakPtr) { RefPtr presShell = do_QueryReferent(aWeakPtr); return !!presShell; } #endif void nsHTMLScrollFrame::SetScrollbarEnabled(Element* aElement, nscoord aMaxPos) { DebugOnly weakShell(do_GetWeakReference(PresShell())); if (aMaxPos) { aElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); } else { aElement->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u"true"_ns, true); } MOZ_ASSERT(ShellIsAlive(weakShell), "pres shell was destroyed by scrolling"); } void nsHTMLScrollFrame::SetCoordAttribute(Element* aElement, nsAtom* aAtom, nscoord aSize) { DebugOnly weakShell(do_GetWeakReference(PresShell())); // convert to pixels int32_t pixelSize = nsPresContext::AppUnitsToIntCSSPixels(aSize); // only set the attribute if it changed. nsAutoString newValue; newValue.AppendInt(pixelSize); if (aElement->AttrValueIs(kNameSpaceID_None, aAtom, newValue, eCaseMatters)) { return; } AutoWeakFrame weakFrame(this); RefPtr kungFuDeathGrip = aElement; aElement->SetAttr(kNameSpaceID_None, aAtom, newValue, true); MOZ_ASSERT(ShellIsAlive(weakShell), "pres shell was destroyed by scrolling"); if (!weakFrame.IsAlive()) { return; } if (mScrollbarActivity && (mHasHorizontalScrollbar || mHasVerticalScrollbar)) { RefPtr 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 nsHTMLScrollFrame::GetBorderRadii(const nsSize& aFrameSize, const nsSize& aBorderArea, Sides aSkipSides, nscoord aRadii[8]) const { if (!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 = GetUsedBorder(); if (sb.left > 0 || sb.top > 0) { ReduceRadii(border.left, border.top, aRadii[eCornerTopLeftX], aRadii[eCornerTopLeftY]); } if (sb.top > 0 || sb.right > 0) { ReduceRadii(border.right, border.top, aRadii[eCornerTopRightX], aRadii[eCornerTopRightY]); } if (sb.right > 0 || sb.bottom > 0) { ReduceRadii(border.right, border.bottom, aRadii[eCornerBottomRightX], aRadii[eCornerBottomRightY]); } if (sb.bottom > 0 || sb.left > 0) { ReduceRadii(border.left, border.bottom, aRadii[eCornerBottomLeftX], aRadii[eCornerBottomLeftY]); } return true; } static nscoord SnapCoord(nscoord aCoord, double aRes, nscoord aAppUnitsPerPixel) { if (StaticPrefs::layout_scroll_disable_pixel_alignment()) { return aCoord; } double snappedToLayerPixels = NS_round((aRes * aCoord) / aAppUnitsPerPixel); return NSToCoordRoundWithClamp(snappedToLayerPixels * aAppUnitsPerPixel / aRes); } nsRect nsHTMLScrollFrame::GetScrolledRect() const { nsRect result = GetUnsnappedScrolledRectInternal( mScrolledFrame->ScrollableOverflowRect(), mScrollPort.Size()); #if 0 // This happens often enough. if (result.width < mScrollPort.width || result.height < mScrollPort.height) { NS_WARNING("Scrolled rect smaller than scrollport?"); } #endif // 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 visualViewportSize = GetVisualViewportSize(); const nsIFrame* referenceFrame = mReferenceFrameDuringPainting ? mReferenceFrameDuringPainting : nsLayoutUtils::GetReferenceFrame( const_cast(this)); nsPoint toReferenceFrame = GetOffsetToCrossDoc(referenceFrame); nsRect scrollPort(mScrollPort.TopLeft() + toReferenceFrame, visualViewportSize); 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(); MatrixScales scale = GetPaintedLayerScaleForFrame( mScrolledFrame, /* aIncludeCSSTransform = */ false); if (scale.xScale == 0 || scale.yScale == 0) { scale = MatrixScales(); } // Compute bounds for the scroll position, and computed the snapped scrolled // rect from the scroll position bounds. nscoord snappedScrolledAreaBottom = SnapCoord(scrolledRect.YMost(), scale.yScale, appUnitsPerDevPixel); nscoord snappedScrollPortBottom = SnapCoord(scrollPort.YMost(), scale.yScale, appUnitsPerDevPixel); nscoord maximumScrollOffsetY = snappedScrolledAreaBottom - snappedScrollPortBottom; result.SetBottomEdge(scrollPort.height + maximumScrollOffsetY); if (GetScrolledFrameDir() == StyleDirection::Ltr) { nscoord snappedScrolledAreaRight = SnapCoord(scrolledRect.XMost(), scale.xScale, appUnitsPerDevPixel); nscoord snappedScrollPortRight = SnapCoord(scrollPort.XMost(), scale.xScale, 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.xScale, appUnitsPerDevPixel); nscoord snappedScrollPortLeft = SnapCoord(scrollPort.x, scale.xScale, appUnitsPerDevPixel); nscoord minimumScrollOffsetX = snappedScrolledAreaLeft - snappedScrollPortLeft; result.SetLeftEdge(minimumScrollOffsetX); } return result; } StyleDirection nsHTMLScrollFrame::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 == StyleUnicodeBidi::Plaintext) { if (nsIFrame* child = mScrolledFrame->PrincipalChildList().FirstChild()) { return nsBidiPresUtils::ParagraphDirection(child) == mozilla::intl::BidiDirection::LTR ? StyleDirection::Ltr : StyleDirection::Rtl; } } return IsBidiLTR() ? StyleDirection::Ltr : StyleDirection::Rtl; } nsRect nsHTMLScrollFrame::GetUnsnappedScrolledRectInternal( const nsRect& aScrolledOverflowArea, const nsSize& aScrollPortSize) const { return nsLayoutUtils::GetScrolledRect(mScrolledFrame, aScrolledOverflowArea, aScrollPortSize, GetScrolledFrameDir()); } nsMargin nsHTMLScrollFrame::GetActualScrollbarSizes( nsIScrollableFrame::ScrollbarSizesOptions aOptions /* = nsIScrollableFrame::ScrollbarSizesOptions::NONE */) const { nsRect r = GetPaddingRectRelativeToSelf(); nsMargin m(mScrollPort.y - r.y, r.XMost() - mScrollPort.XMost(), r.YMost() - mScrollPort.YMost(), mScrollPort.x - r.x); if (aOptions == nsIScrollableFrame::ScrollbarSizesOptions:: INCLUDE_VISUAL_VIEWPORT_SCROLLBARS && !UsesOverlayScrollbars()) { // If we are using layout scrollbars and they only exist to scroll the // visual viewport then they do not take up any layout space (so the // scrollport is the same as the padding rect) but they do cover everything // below them so some callers may want to include this special type of // scrollbars in the returned value. if (mHScrollbarBox && mHasHorizontalScrollbar && mOnlyNeedHScrollbarToScrollVVInsideLV) { m.bottom += mHScrollbarBox->GetRect().height; } if (mVScrollbarBox && mHasVerticalScrollbar && mOnlyNeedVScrollbarToScrollVVInsideLV) { if (IsScrollbarOnRight()) { m.right += mVScrollbarBox->GetRect().width; } else { m.left += mVScrollbarBox->GetRect().width; } } } return m; } void nsHTMLScrollFrame::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 nsHTMLScrollFrame::GetCoordAttribute(nsIFrame* aBox, nsAtom* aAtom, nscoord aDefaultValue, nscoord* aRangeStart, nscoord* aRangeLength) { if (aBox) { nsIContent* content = aBox->GetContent(); nsAutoString value; if (content->IsElement()) { content->AsElement()->GetAttr(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; } bool nsHTMLScrollFrame::IsLastScrollUpdateAnimating() const { if (!mScrollUpdates.IsEmpty()) { switch (mScrollUpdates.LastElement().GetMode()) { case ScrollMode::Smooth: case ScrollMode::SmoothMsd: return true; case ScrollMode::Instant: case ScrollMode::Normal: break; } } return false; } bool nsHTMLScrollFrame::IsLastScrollUpdateTriggeredByScriptAnimating() const { if (!mScrollUpdates.IsEmpty()) { const ScrollPositionUpdate& lastUpdate = mScrollUpdates.LastElement(); if (lastUpdate.WasTriggeredByScript() && (mScrollUpdates.LastElement().GetMode() == ScrollMode::Smooth || mScrollUpdates.LastElement().GetMode() == ScrollMode::SmoothMsd)) { return true; } } return false; } using AnimationState = nsIScrollableFrame::AnimationState; EnumSet nsHTMLScrollFrame::ScrollAnimationState() const { EnumSet retval; if (IsApzAnimationInProgress()) { retval += AnimationState::APZInProgress; if (mCurrentAPZScrollAnimationType == APZScrollAnimationType::TriggeredByScript) { retval += AnimationState::TriggeredByScript; } } if (mApzAnimationRequested) { retval += AnimationState::APZRequested; if (mApzAnimationTriggeredByScriptRequested) { retval += AnimationState::TriggeredByScript; } } if (IsLastScrollUpdateAnimating()) { retval += AnimationState::APZPending; if (IsLastScrollUpdateTriggeredByScriptAnimating()) { retval += AnimationState::TriggeredByScript; } } if (mAsyncScroll) { retval += AnimationState::MainThread; if (mAsyncScroll->WasTriggeredByScript()) { retval += AnimationState::TriggeredByScript; } } if (mAsyncSmoothMSDScroll) { retval += AnimationState::MainThread; if (mAsyncSmoothMSDScroll->WasTriggeredByScript()) { retval += AnimationState::TriggeredByScript; } } return retval; } void nsHTMLScrollFrame::ResetScrollInfoIfNeeded( const MainThreadScrollGeneration& aGeneration, const APZScrollGeneration& aGenerationOnApz, APZScrollAnimationType aAPZScrollAnimationType, InScrollingGesture aInScrollingGesture) { if (aGeneration == mScrollGeneration) { mLastScrollOrigin = ScrollOrigin::None; mApzAnimationRequested = false; mApzAnimationTriggeredByScriptRequested = false; } mScrollGenerationOnApz = aGenerationOnApz; // We can reset this regardless of scroll generation, as this is only set // here, as a response to APZ requesting a repaint. mCurrentAPZScrollAnimationType = aAPZScrollAnimationType; mInScrollingGesture = aInScrollingGesture; } UniquePtr nsHTMLScrollFrame::SaveState() { 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. auto scrollAnimationState = ScrollAnimationState(); bool isScrollAnimating = scrollAnimationState.contains(AnimationState::MainThread) || scrollAnimationState.contains(AnimationState::APZPending) || scrollAnimationState.contains(AnimationState::APZRequested); if (!mHasBeenScrolled && !mDidHistoryRestore && !isScrollAnimating) { return nullptr; } UniquePtr state = NewPresState(); 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 = GetLogicalVisualViewportOffset(); if (isScrollAnimating) { pt = mDestination; allowScrollOriginDowngrade = false; } SCROLLRESTORE_LOG("%p: SaveState, pt=%s, mLastPos=%s, mRestorePos=%s\n", this, ToString(pt).c_str(), ToString(mLastPos).c_str(), ToString(mRestorePos).c_str()); if (mRestorePos.y != -1 && pt == mLastPos) { pt = mRestorePos; } state->scrollState() = pt; state->allowScrollOriginDowngrade() = allowScrollOriginDowngrade; if (mIsRoot) { // Only save resolution properties for root scroll frames state->resolution() = PresShell()->GetResolution(); } return state; } NS_IMETHODIMP nsHTMLScrollFrame::RestoreState(PresState* aState) { mRestorePos = aState->scrollState(); MOZ_ASSERT(mLastScrollOrigin == ScrollOrigin::None); mAllowScrollOriginDowngrade = aState->allowScrollOriginDowngrade(); // When restoring state, we promote mLastScrollOrigin to a stronger value // from the default of eNone, to restore the behaviour that existed when // the state was saved. If mLastScrollOrigin was a weaker value previously, // then mAllowScrollOriginDowngrade will be true, and so the combination of // mAllowScrollOriginDowngrade and the stronger mLastScrollOrigin will allow // the same types of scrolls as before. It might be possible to also just // save and restore the mAllowScrollOriginDowngrade and mLastScrollOrigin // values directly without this sort of fiddling. Something to try in the // future or if we tinker with this code more. mLastScrollOrigin = ScrollOrigin::Other; mDidHistoryRestore = true; mLastPos = mScrolledFrame ? GetLogicalVisualViewportOffset() : nsPoint(0, 0); SCROLLRESTORE_LOG("%p: RestoreState, set mRestorePos=%s mLastPos=%s\n", this, ToString(mRestorePos).c_str(), ToString(mLastPos).c_str()); // Resolution properties should only exist on root scroll frames. MOZ_ASSERT(mIsRoot || aState->resolution() == 1.0); if (mIsRoot) { PresShell()->SetResolutionAndScaleTo( aState->resolution(), ResolutionChangeOrigin::MainThreadRestore); } return NS_OK; } void nsHTMLScrollFrame::PostScrolledAreaEvent() { if (mScrolledAreaEvent.IsPending()) { return; } mScrolledAreaEvent = new ScrolledAreaEvent(this); nsContentUtils::AddScriptRunner(mScrolledAreaEvent.get()); } //////////////////////////////////////////////////////////////////////////////// // ScrolledArea change event dispatch // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsHTMLScrollFrame::ScrolledAreaEvent::Run() { if (mHelper) { mHelper->FireScrolledAreaEvent(); } return NS_OK; } void nsHTMLScrollFrame::FireScrolledAreaEvent() { mScrolledAreaEvent.Forget(); InternalScrollAreaEvent event(true, eScrolledAreaChanged, nullptr); RefPtr presContext = PresContext(); nsIContent* content = GetContent(); event.mArea = mScrolledFrame->ScrollableOverflowRectRelativeToParent(); if (RefPtr doc = content->GetUncomposedDoc()) { EventDispatcher::Dispatch(doc, presContext, &event, nullptr); } } ScrollDirections nsIScrollableFrame::GetAvailableScrollingDirections() const { nscoord oneDevPixel = GetScrolledFrame()->PresContext()->AppUnitsPerDevPixel(); ScrollDirections directions; nsRect scrollRange = GetScrollRange(); if (scrollRange.width >= oneDevPixel) { directions += ScrollDirection::eHorizontal; } if (scrollRange.height >= oneDevPixel) { directions += ScrollDirection::eVertical; } return directions; } nsRect nsHTMLScrollFrame::GetScrollRangeForUserInputEvents() const { // This function computes a scroll range based on a scrolled rect and scroll // port defined as follows: // scrollable rect = overflow:hidden ? layout viewport : scrollable rect // scroll port = have visual viewport ? visual viewport : layout viewport // The results in the same notion of scroll range that APZ uses (the combined // effect of FrameMetrics::CalculateScrollRange() and // nsLayoutUtils::CalculateScrollableRectForFrame). ScrollStyles ss = GetScrollStyles(); nsPoint scrollPos = GetScrollPosition(); nsRect scrolledRect = GetScrolledRect(); if (StyleOverflow::Hidden == ss.mHorizontal) { scrolledRect.width = mScrollPort.width; scrolledRect.x = scrollPos.x; } if (StyleOverflow::Hidden == ss.mVertical) { scrolledRect.height = mScrollPort.height; scrolledRect.y = scrollPos.y; } nsSize scrollPort = GetVisualViewportSize(); nsRect scrollRange = scrolledRect; scrollRange.width = std::max(scrolledRect.width - scrollPort.width, 0); scrollRange.height = std::max(scrolledRect.height - scrollPort.height, 0); return scrollRange; } ScrollDirections nsHTMLScrollFrame::GetAvailableScrollingDirectionsForUserInputEvents() const { nsRect scrollRange = GetScrollRangeForUserInputEvents(); // We check if there is at least one half of a screen pixel of scroll range to // roughly match what apz does when it checks if the change in scroll position // in screen pixels round to zero or not. // (https://searchfox.org/mozilla-central/rev/2f09184ec781a2667feec87499d4b81b32b6c48e/gfx/layers/apz/src/AsyncPanZoomController.cpp#3210) // This isn't quite half a screen pixel, it doesn't take into account CSS // transforms, but should be good enough. float halfScreenPixel = GetScrolledFrame()->PresContext()->AppUnitsPerDevPixel() / (PresShell()->GetCumulativeResolution() * 2.f); ScrollDirections directions; if (scrollRange.width >= halfScreenPixel) { directions += ScrollDirection::eHorizontal; } if (scrollRange.height >= halfScreenPixel) { directions += ScrollDirection::eVertical; } return directions; } /** * Append scroll positions for valid snap positions into |aSnapInfo| if * applicable. */ static void AppendScrollPositionsForSnap( const nsIFrame* aFrame, const nsIFrame* aScrolledFrame, const nsRect& aScrolledRect, const nsMargin& aScrollPadding, const nsRect& aScrollRange, WritingMode aWritingModeOnScroller, ScrollSnapInfo& aSnapInfo, nsHTMLScrollFrame::SnapTargetSet* aSnapTargets) { ScrollSnapTargetId targetId = ScrollSnapUtils::GetTargetIdFor(aFrame); nsRect snapArea = ScrollSnapUtils::GetSnapAreaFor(aFrame, aScrolledFrame, aScrolledRect); // Use the writing-mode on the target element if the snap area is larger than // the snapport. // https://drafts.csswg.org/css-scroll-snap/#snap-scope WritingMode writingMode = ScrollSnapUtils::NeedsToRespectTargetWritingMode( snapArea.Size(), aSnapInfo.mSnapportSize) ? aFrame->GetWritingMode() : aWritingModeOnScroller; // These snap range shouldn't be involved with scroll-margin since we just // need the visible range of the target element. if (snapArea.width > aSnapInfo.mSnapportSize.width) { aSnapInfo.mXRangeWiderThanSnapport.AppendElement( ScrollSnapInfo::ScrollSnapRange(snapArea, ScrollDirection::eHorizontal, targetId)); } if (snapArea.height > aSnapInfo.mSnapportSize.height) { aSnapInfo.mYRangeWiderThanSnapport.AppendElement( ScrollSnapInfo::ScrollSnapRange(snapArea, ScrollDirection::eVertical, targetId)); } // Shift target rect position by the scroll padding to get the padded // position thus we don't need to take account scroll-padding values in // ScrollSnapUtils::GetSnapPointForDestination() when it gets called from // the compositor thread. snapArea.y -= aScrollPadding.top; snapArea.x -= aScrollPadding.left; LogicalRect logicalTargetRect(writingMode, snapArea, aSnapInfo.mSnapportSize); LogicalSize logicalSnapportRect(writingMode, aSnapInfo.mSnapportSize); LogicalRect logicalScrollRange(aWritingModeOnScroller, aScrollRange, // The origin of this logical coordinate system // what we need here is (0, 0), so we use an // empty size. nsSize()); Maybe blockDirectionPosition; const nsStyleDisplay* styleDisplay = aFrame->StyleDisplay(); nscoord containerBSize = logicalSnapportRect.BSize(writingMode); switch (styleDisplay->mScrollSnapAlign.block) { case StyleScrollSnapAlignKeyword::None: break; case StyleScrollSnapAlignKeyword::Start: blockDirectionPosition.emplace( writingMode.IsVerticalRL() ? -logicalTargetRect.BStart(writingMode) : logicalTargetRect.BStart(writingMode)); break; case StyleScrollSnapAlignKeyword::End: { nscoord candidate = std::clamp( // What we need here is the scroll position instead of the snap // position itself, so we need, for example, the top edge of the // scroll port on horizontal-tb when the frame is positioned at // the bottom edge of the scroll port. For this reason we subtract // containerBSize from BEnd of the target and clamp it inside the // scrollable range. logicalTargetRect.BEnd(writingMode) - containerBSize, logicalScrollRange.BStart(writingMode), logicalScrollRange.BEnd(writingMode)); blockDirectionPosition.emplace(writingMode.IsVerticalRL() ? -candidate : candidate); break; } case StyleScrollSnapAlignKeyword::Center: { nscoord targetCenter = (logicalTargetRect.BStart(writingMode) + logicalTargetRect.BEnd(writingMode)) / 2; nscoord halfSnapportSize = containerBSize / 2; // Get the center of the target to align with the center of the snapport // depending on direction. nscoord candidate = std::clamp(targetCenter - halfSnapportSize, logicalScrollRange.BStart(writingMode), logicalScrollRange.BEnd(writingMode)); blockDirectionPosition.emplace(writingMode.IsVerticalRL() ? -candidate : candidate); break; } } Maybe inlineDirectionPosition; nscoord containerISize = logicalSnapportRect.ISize(writingMode); switch (styleDisplay->mScrollSnapAlign.inline_) { case StyleScrollSnapAlignKeyword::None: break; case StyleScrollSnapAlignKeyword::Start: inlineDirectionPosition.emplace( writingMode.IsInlineReversed() ? -logicalTargetRect.IStart(writingMode) : logicalTargetRect.IStart(writingMode)); break; case StyleScrollSnapAlignKeyword::End: { nscoord candidate = std::clamp( // Same as above BEnd case, we subtract containerISize. // // Note that the logical scroll range is mapped to [0, x] range even // if it's in RTL contents. So for example, if the physical range is // [-200, 0], it's mapped to [0, 200], i.e. IStart() is 0, IEnd() is // 200. So we can just use std::clamp with the same arguments in both // RTL/LTR cases. logicalTargetRect.IEnd(writingMode) - containerISize, logicalScrollRange.IStart(writingMode), logicalScrollRange.IEnd(writingMode)); inlineDirectionPosition.emplace( writingMode.IsInlineReversed() ? -candidate : candidate); break; } case StyleScrollSnapAlignKeyword::Center: { nscoord targetCenter = (logicalTargetRect.IStart(writingMode) + logicalTargetRect.IEnd(writingMode)) / 2; nscoord halfSnapportSize = containerISize / 2; // Get the center of the target to align with the center of the snapport // depending on direction. nscoord candidate = std::clamp(targetCenter - halfSnapportSize, logicalScrollRange.IStart(writingMode), logicalScrollRange.IEnd(writingMode)); inlineDirectionPosition.emplace( writingMode.IsInlineReversed() ? -candidate : candidate); break; } } if (blockDirectionPosition || inlineDirectionPosition) { aSnapInfo.mSnapTargets.AppendElement( writingMode.IsVertical() ? ScrollSnapInfo::SnapTarget( std::move(blockDirectionPosition), std::move(inlineDirectionPosition), std::move(snapArea), styleDisplay->mScrollSnapStop, targetId) : ScrollSnapInfo::SnapTarget( std::move(inlineDirectionPosition), std::move(blockDirectionPosition), std::move(snapArea), styleDisplay->mScrollSnapStop, targetId)); if (aSnapTargets) { aSnapTargets->EnsureInserted(aFrame->GetContent()); } } } /** * Collect the scroll positions corresponding to snap positions of frames in the * subtree rooted at |aFrame|, relative to |aScrolledFrame|, into |aSnapInfo|. */ static void CollectScrollPositionsForSnap( nsIFrame* aFrame, nsIFrame* aScrolledFrame, const nsRect& aScrolledRect, const nsMargin& aScrollPadding, const nsRect& aScrollRange, WritingMode aWritingModeOnScroller, ScrollSnapInfo& aSnapInfo, nsHTMLScrollFrame::SnapTargetSet* aSnapTargets) { // Snap positions only affect the nearest ancestor scroll container on the // element's containing block chain. nsIScrollableFrame* sf = do_QueryFrame(aFrame); if (sf) { return; } for (const auto& childList : aFrame->ChildLists()) { for (nsIFrame* f : childList.mList) { const nsStyleDisplay* styleDisplay = f->StyleDisplay(); if (styleDisplay->mScrollSnapAlign.inline_ != StyleScrollSnapAlignKeyword::None || styleDisplay->mScrollSnapAlign.block != StyleScrollSnapAlignKeyword::None) { AppendScrollPositionsForSnap( f, aScrolledFrame, aScrolledRect, aScrollPadding, aScrollRange, aWritingModeOnScroller, aSnapInfo, aSnapTargets); } CollectScrollPositionsForSnap( f, aScrolledFrame, aScrolledRect, aScrollPadding, aScrollRange, aWritingModeOnScroller, aSnapInfo, aSnapTargets); } } } static nscoord ResolveScrollPaddingStyleValue( const StyleRect& aScrollPaddingStyle, Side aSide, const nsSize& aScrollPortSize) { if (aScrollPaddingStyle.Get(aSide).IsAuto()) { // https://drafts.csswg.org/css-scroll-snap-1/#valdef-scroll-padding-auto return 0; } nscoord percentageBasis; switch (aSide) { case eSideTop: case eSideBottom: percentageBasis = aScrollPortSize.height; break; case eSideLeft: case eSideRight: percentageBasis = aScrollPortSize.width; break; } return aScrollPaddingStyle.Get(aSide).AsLengthPercentage().Resolve( percentageBasis); } static nsMargin ResolveScrollPaddingStyle( const StyleRect& aScrollPaddingStyle, const nsSize& aScrollPortSize) { return nsMargin(ResolveScrollPaddingStyleValue(aScrollPaddingStyle, eSideTop, aScrollPortSize), ResolveScrollPaddingStyleValue(aScrollPaddingStyle, eSideRight, aScrollPortSize), ResolveScrollPaddingStyleValue(aScrollPaddingStyle, eSideBottom, aScrollPortSize), ResolveScrollPaddingStyleValue(aScrollPaddingStyle, eSideLeft, aScrollPortSize)); } nsMargin nsHTMLScrollFrame::GetScrollPadding() const { nsIFrame* styleFrame = GetFrameForStyle(); if (!styleFrame) { return nsMargin(); } // The spec says percentage values are relative to the scroll port size. // https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding return ResolveScrollPaddingStyle(styleFrame->StylePadding()->mScrollPadding, GetScrollPortRect().Size()); } ScrollSnapInfo nsHTMLScrollFrame::ComputeScrollSnapInfo() { ScrollSnapInfo result; nsIFrame* scrollSnapFrame = GetFrameForStyle(); if (!scrollSnapFrame) { return result; } const nsStyleDisplay* disp = scrollSnapFrame->StyleDisplay(); if (disp->mScrollSnapType.strictness == StyleScrollSnapStrictness::None) { // We won't be snapping, short-circuit the computation. return result; } WritingMode writingMode = GetWritingMode(); result.InitializeScrollSnapStrictness(writingMode, disp); result.mSnapportSize = GetSnapportSize(); CollectScrollPositionsForSnap( mScrolledFrame, mScrolledFrame, GetScrolledRect(), GetScrollPadding(), GetLayoutScrollRange(), writingMode, result, &mSnapTargets); return result; } ScrollSnapInfo nsHTMLScrollFrame::GetScrollSnapInfo() { // TODO(botond): Should we cache it? return ComputeScrollSnapInfo(); } Maybe nsHTMLScrollFrame::GetSnapPointForDestination( ScrollUnit aUnit, ScrollSnapFlags aFlags, const nsPoint& aStartPos, const nsPoint& aDestination) { // We can release the strong references for the previous snap target // elements here since calling this ComputeScrollSnapInfo means we are going // to evaluate new snap points, thus there's no chance to generating // nsIContent instances in between this function call and the function call // for the (re-)evaluation. mSnapTargets.Clear(); return ScrollSnapUtils::GetSnapPointForDestination( ComputeScrollSnapInfo(), aUnit, aFlags, GetLayoutScrollRange(), aStartPos, aDestination); } Maybe nsHTMLScrollFrame::GetSnapPointForResnap() { // Same as in GetSnapPointForDestination, We can release the strong references // for the previous snap targets here. mSnapTargets.Clear(); nsIContent* focusedContent = GetContent()->GetComposedDoc()->GetUnretargetedFocusedContent(); return ScrollSnapUtils::GetSnapPointForResnap( ComputeScrollSnapInfo(), GetLayoutScrollRange(), GetScrollPosition(), mLastSnapTargetIds, focusedContent); } bool nsHTMLScrollFrame::NeedsResnap() { nsIContent* focusedContent = GetContent()->GetComposedDoc()->GetUnretargetedFocusedContent(); return ScrollSnapUtils::GetSnapPointForResnap( ComputeScrollSnapInfo(), GetLayoutScrollRange(), GetScrollPosition(), mLastSnapTargetIds, focusedContent) .isSome(); } void nsHTMLScrollFrame::SetLastSnapTargetIds( UniquePtr aIds) { if (!aIds) { mLastSnapTargetIds = nullptr; return; } // This SetLastSnapTargetIds gets called asynchronously so that there's a race // condition something like; // 1) an async scroll operation triggered snapping to a point on an element // 2) during the async scroll operation, the element got removed from this // scroll container // 3) re-snapping triggered // 4) this SetLastSnapTargetIds got called // In such case |aIds| are stale, we shouldn't use it. for (const auto* idList : {&aIds->mIdsOnX, &aIds->mIdsOnY}) { for (const auto id : *idList) { if (!mSnapTargets.Contains(reinterpret_cast(id))) { mLastSnapTargetIds = nullptr; return; } } } mLastSnapTargetIds = std::move(aIds); } bool nsHTMLScrollFrame::IsLastSnappedTarget(const nsIFrame* aFrame) const { ScrollSnapTargetId id = ScrollSnapUtils::GetTargetIdFor(aFrame); MOZ_ASSERT(id != ScrollSnapTargetId::None, "This function is supposed to be called for contents"); if (!mLastSnapTargetIds) { return false; } return mLastSnapTargetIds->mIdsOnX.Contains(id) || mLastSnapTargetIds->mIdsOnY.Contains(id); } void nsHTMLScrollFrame::TryResnap() { // If there's any async scroll is running or we are processing pan/touch // gestures or scroll thumb dragging, don't clobber the scroll. if (!ScrollAnimationState().isEmpty() || mInScrollingGesture == InScrollingGesture::Yes) { return; } if (auto snapDestination = GetSnapPointForResnap()) { // We are going to re-snap so that we need to clobber scroll anchoring. mAnchor.UserScrolled(); // Snap to the nearest snap position if exists. ScrollToWithOrigin( snapDestination->mPosition, nullptr /* range */, ScrollOperationParams{ IsSmoothScroll(ScrollBehavior::Auto) ? ScrollMode::SmoothMsd : ScrollMode::Instant, ScrollOrigin::Other, std::move(snapDestination->mTargetIds)}); } } void nsHTMLScrollFrame::PostPendingResnapIfNeeded(const nsIFrame* aFrame) { if (!IsLastSnappedTarget(aFrame)) { return; } PostPendingResnap(); } void nsHTMLScrollFrame::PostPendingResnap() { PresShell()->PostPendingScrollResnap(this); } nsIScrollableFrame::PhysicalScrollSnapAlign nsHTMLScrollFrame::GetScrollSnapAlignFor(const nsIFrame* aFrame) const { StyleScrollSnapAlignKeyword alignForY = StyleScrollSnapAlignKeyword::None; StyleScrollSnapAlignKeyword alignForX = StyleScrollSnapAlignKeyword::None; nsIFrame* styleFrame = GetFrameForStyle(); if (!styleFrame) { return {alignForX, alignForY}; } if (styleFrame->StyleDisplay()->mScrollSnapType.strictness == StyleScrollSnapStrictness::None) { return {alignForX, alignForY}; } const nsStyleDisplay* styleDisplay = aFrame->StyleDisplay(); if (styleDisplay->mScrollSnapAlign.inline_ == StyleScrollSnapAlignKeyword::None && styleDisplay->mScrollSnapAlign.block == StyleScrollSnapAlignKeyword::None) { return {alignForX, alignForY}; } nsSize snapAreaSize = ScrollSnapUtils::GetSnapAreaFor(aFrame, mScrolledFrame, GetScrolledRect()) .Size(); const WritingMode writingMode = ScrollSnapUtils::NeedsToRespectTargetWritingMode(snapAreaSize, GetSnapportSize()) ? aFrame->GetWritingMode() : styleFrame->GetWritingMode(); switch (styleFrame->StyleDisplay()->mScrollSnapType.axis) { case StyleScrollSnapAxis::X: alignForX = writingMode.IsVertical() ? styleDisplay->mScrollSnapAlign.block : styleDisplay->mScrollSnapAlign.inline_; break; case StyleScrollSnapAxis::Y: alignForY = writingMode.IsVertical() ? styleDisplay->mScrollSnapAlign.inline_ : styleDisplay->mScrollSnapAlign.block; break; case StyleScrollSnapAxis::Block: if (writingMode.IsVertical()) { alignForX = styleDisplay->mScrollSnapAlign.block; } else { alignForY = styleDisplay->mScrollSnapAlign.block; } break; case StyleScrollSnapAxis::Inline: if (writingMode.IsVertical()) { alignForY = styleDisplay->mScrollSnapAlign.inline_; } else { alignForX = styleDisplay->mScrollSnapAlign.inline_; } break; case StyleScrollSnapAxis::Both: if (writingMode.IsVertical()) { alignForX = styleDisplay->mScrollSnapAlign.block; alignForY = styleDisplay->mScrollSnapAlign.inline_; } else { alignForX = styleDisplay->mScrollSnapAlign.inline_; alignForY = styleDisplay->mScrollSnapAlign.block; } break; } return {alignForX, alignForY}; } bool nsHTMLScrollFrame::UsesOverlayScrollbars() const { return PresContext()->UseOverlayScrollbars(); } bool nsHTMLScrollFrame::DragScroll(WidgetEvent* aEvent) { // Dragging is allowed while within a 20 pixel border. Note that device pixels // are used so that the same margin is used even when zoomed in or out. nscoord margin = 20 * PresContext()->AppUnitsPerDevPixel(); // Don't drag scroll for small scrollareas. if (mScrollPort.width < margin * 2 || mScrollPort.height < margin * 2) { return false; } // If willScroll is computed as false, then the frame is already scrolled as // far as it can go in both directions. Return false so that an ancestor // scrollframe can scroll instead. bool willScroll = false; nsPoint pnt = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this}); nsPoint scrollPoint = GetScrollPosition(); nsRect rangeRect = GetLayoutScrollRange(); // Only drag scroll when a scrollbar is present. nsPoint offset; if (mHasHorizontalScrollbar) { if (pnt.x >= mScrollPort.x && pnt.x <= mScrollPort.x + margin) { offset.x = -margin; if (scrollPoint.x > 0) { willScroll = true; } } else if (pnt.x >= mScrollPort.XMost() - margin && pnt.x <= mScrollPort.XMost()) { offset.x = margin; if (scrollPoint.x < rangeRect.width) { willScroll = true; } } } if (mHasVerticalScrollbar) { if (pnt.y >= mScrollPort.y && pnt.y <= mScrollPort.y + margin) { offset.y = -margin; if (scrollPoint.y > 0) { willScroll = true; } } else if (pnt.y >= mScrollPort.YMost() - margin && pnt.y <= mScrollPort.YMost()) { offset.y = margin; if (scrollPoint.y < rangeRect.height) { willScroll = true; } } } if (offset.x || offset.y) { ScrollToWithOrigin( GetScrollPosition() + offset, nullptr /* range */, ScrollOperationParams{ScrollMode::Normal, ScrollOrigin::Other}); } return willScroll; } static nsSliderFrame* GetSliderFrame(nsIFrame* aScrollbarFrame) { if (!aScrollbarFrame) { return nullptr; } for (const auto& childList : aScrollbarFrame->ChildLists()) { for (nsIFrame* frame : childList.mList) { if (nsSliderFrame* sliderFrame = do_QueryFrame(frame)) { return sliderFrame; } } } return nullptr; } static void AsyncScrollbarDragInitiated(uint64_t aDragBlockId, nsIFrame* aScrollbar) { if (nsSliderFrame* sliderFrame = GetSliderFrame(aScrollbar)) { sliderFrame->AsyncScrollbarDragInitiated(aDragBlockId); } } void nsHTMLScrollFrame::AsyncScrollbarDragInitiated( uint64_t aDragBlockId, ScrollDirection aDirection) { switch (aDirection) { case ScrollDirection::eVertical: ::AsyncScrollbarDragInitiated(aDragBlockId, mVScrollbarBox); break; case ScrollDirection::eHorizontal: ::AsyncScrollbarDragInitiated(aDragBlockId, mHScrollbarBox); break; } } static void AsyncScrollbarDragRejected(nsIFrame* aScrollbar) { if (nsSliderFrame* sliderFrame = GetSliderFrame(aScrollbar)) { sliderFrame->AsyncScrollbarDragRejected(); } } void nsHTMLScrollFrame::AsyncScrollbarDragRejected() { // We don't get told which scrollbar requested the async drag, // so we notify both. ::AsyncScrollbarDragRejected(mHScrollbarBox); ::AsyncScrollbarDragRejected(mVScrollbarBox); } void nsHTMLScrollFrame::ApzSmoothScrollTo( const nsPoint& aDestination, ScrollMode aMode, ScrollOrigin aOrigin, ScrollTriggeredByScript aTriggeredByScript, UniquePtr aSnapTargetIds) { if (mApzSmoothScrollDestination == Some(aDestination)) { // If we already sent APZ a smooth-scroll request to this // destination (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. 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. MOZ_ASSERT(aOrigin != ScrollOrigin::None); mApzSmoothScrollDestination = Some(aDestination); AppendScrollUpdate(ScrollPositionUpdate::NewSmoothScroll( aMode, aOrigin, aDestination, aTriggeredByScript, std::move(aSnapTargetIds))); nsIContent* content = GetContent(); if (!DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(content)) { // 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. if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug)) { mozilla::layers::ScrollableLayerGuid::ViewID viewID = mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID; nsLayoutUtils::FindIDFor(content, &viewID); MOZ_LOG( sDisplayportLog, LogLevel::Debug, ("ApzSmoothScrollTo setting displayport on scrollId=%" PRIu64 "\n", viewID)); } DisplayPortUtils::CalculateAndSetDisplayPortMargins( GetScrollTargetFrame(), DisplayPortUtils::RepaintMode::Repaint); nsIFrame* frame = do_QueryFrame(GetScrollTargetFrame()); DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(frame); } // Schedule a paint to ensure that the frame metrics get updated on // the compositor thread. SchedulePaint(); } bool nsHTMLScrollFrame::CanApzScrollInTheseDirections( ScrollDirections aDirections) { ScrollStyles styles = GetScrollStyles(); if (aDirections.contains(ScrollDirection::eHorizontal) && styles.mHorizontal == StyleOverflow::Hidden) return false; if (aDirections.contains(ScrollDirection::eVertical) && styles.mVertical == StyleOverflow::Hidden) return false; return true; } bool nsHTMLScrollFrame::SmoothScrollVisual( const nsPoint& aVisualViewportOffset, FrameMetrics::ScrollOffsetUpdateType aUpdateType) { bool canDoApzSmoothScroll = nsLayoutUtils::AsyncPanZoomEnabled(this) && WantAsyncScroll(); if (!canDoApzSmoothScroll) { return false; } // Clamp the destination to the visual scroll range. // There is a potential issue here, where |mDestination| is usually // clamped to the layout scroll range, and so e.g. a subsequent // window.scrollBy() may have an undesired effect. However, as this function // is only called internally, this should not be a problem in practice. // If it turns out to be, the fix would be: // - add a new "destination" field that doesn't have to be clamped to // the layout scroll range // - clamp mDestination to the layout scroll range here // - make sure ComputeScrollMetadata() picks up the former as the // smooth scroll destination to send to APZ. mDestination = GetVisualScrollRange().ClampPoint(aVisualViewportOffset); UniquePtr snapTargetIds; // Perform the scroll. ApzSmoothScrollTo(mDestination, ScrollMode::SmoothMsd, aUpdateType == FrameMetrics::eRestore ? ScrollOrigin::Restore : ScrollOrigin::Other, ScrollTriggeredByScript::No, std::move(snapTargetIds)); return true; } bool nsHTMLScrollFrame::IsSmoothScroll(dom::ScrollBehavior aBehavior) const { if (aBehavior == dom::ScrollBehavior::Instant) { return false; } // The user smooth scrolling preference should be honored for any requested // smooth scrolls. A requested smooth scroll when smooth scrolling is // disabled should be equivalent to an instant scroll. This is not enforced // for the XUL element to allow for the browser chrome to // override this behavior when toolkit.scrollbox.smoothScroll is enabled. if (!GetContent()->IsXULElement(nsGkAtoms::scrollbox)) { if (!nsLayoutUtils::IsSmoothScrollingEnabled()) { return false; } } else { if (!StaticPrefs::toolkit_scrollbox_smoothScroll()) { return false; } } if (aBehavior == dom::ScrollBehavior::Smooth) { return true; } nsIFrame* styleFrame = GetFrameForStyle(); if (!styleFrame) { return false; } return (aBehavior == dom::ScrollBehavior::Auto && styleFrame->StyleDisplay()->mScrollBehavior == StyleScrollBehavior::Smooth); } nsTArray nsHTMLScrollFrame::GetScrollUpdates() const { return mScrollUpdates.Clone(); } void nsHTMLScrollFrame::AppendScrollUpdate( const ScrollPositionUpdate& aUpdate) { mScrollGeneration = aUpdate.GetGeneration(); mScrollUpdates.AppendElement(aUpdate); } void nsHTMLScrollFrame::ScheduleScrollAnimations() { nsIContent* content = GetContent(); MOZ_ASSERT(content && content->IsElement(), "The nsIScrollableFrame should have the element."); const Element* elementOrPseudo = content->AsElement(); PseudoStyleType pseudo = elementOrPseudo->GetPseudoElementType(); if (pseudo != PseudoStyleType::NotPseudo && !AnimationUtils::IsSupportedPseudoForAnimations(pseudo)) { // This is not an animatable pseudo element, and so we don't generate // scroll-timeline for it. return; } const auto [element, type] = AnimationUtils::GetElementPseudoPair(elementOrPseudo); const auto* scheduler = ProgressTimelineScheduler::Get(element, type); if (!scheduler) { // We don't have scroll timelines associated with this frame. return; } scheduler->ScheduleAnimations(); }